From ad4130d5599664de1b461db36c4ff56a6cc39f5e Mon Sep 17 00:00:00 2001 From: Axelander Date: Mon, 14 Dec 2020 09:14:13 +0000 Subject: [PATCH 01/50] Merge latest features with pre-release --- .gitignore | 6 +- CHANGELOG.md | 5 + OpenBudgeteer.Blazor/Dockerfile | 2 + .../OpenBudgeteer.Blazor.csproj | 29 +- OpenBudgeteer.Blazor/Pages/Account.razor | 12 +- OpenBudgeteer.Blazor/Pages/Bucket.razor | 12 +- OpenBudgeteer.Blazor/Pages/Import.razor | 18 +- OpenBudgeteer.Blazor/Pages/Report.razor | 2 +- OpenBudgeteer.Blazor/Pages/Rules.razor | 10 +- OpenBudgeteer.Blazor/Pages/Transaction.razor | 10 +- OpenBudgeteer.Blazor/Shared/NavMenu.razor | 2 +- OpenBudgeteer.Blazor/Startup.cs | 47 +- .../ViewModels/BlazorReportViewModel.cs | 2 +- OpenBudgeteer.Blazor/appsettings.json | 3 +- OpenBudgeteer.Core.Test/DbConnector.cs | 49 + .../OpenBudgeteer.Core.Test.csproj | 24 + .../AccountViewModelIsolatedTest.cs | 47 + .../ViewModelTest/AccountViewModelTest.cs | 71 ++ .../BucketViewModelIsolatedTest.cs | 142 +++ .../ViewModelTest/BucketViewModelTest.cs | 895 ++++++++++++++++++ .../YearMonthSelectorViewModelTest.cs | 123 +++ .../Common/{ => Database}/DatabaseContext.cs | 4 +- .../Common/Database/MySqlDatabaseContext.cs | 15 + .../Database/MySqlDatabaseContextFactory.cs | 39 + .../Common/Database/SqliteDatabaseContext.cs | 15 + .../Database/SqliteDatabaseContextFactory.cs | 29 + .../Common/ViewModelOperationResult.cs | 29 + .../20200605093534_InitialCreate.Designer.cs | 6 +- .../MySql}/20200605093534_InitialCreate.cs | 2 +- ...0200608152707_DecimalPrecision.Designer.cs | 6 +- .../MySql}/20200608152707_DecimalPrecision.cs | 2 +- ...00612082229_FixedBucketVersion.Designer.cs | 6 +- .../20200612082229_FixedBucketVersion.cs | 2 +- ...04_DefaultIncomeTransferBucket.Designer.cs | 6 +- ...00622121904_DefaultIncomeTransferBucket.cs | 2 +- .../20200701071320_BucketColor.Designer.cs | 6 +- .../MySql}/20200701071320_BucketColor.cs | 2 +- ...0200701133458_DateTimeDataType.Designer.cs | 6 +- .../MySql}/20200701133458_DateTimeDataType.cs | 2 +- ...4081215_ImportProfileDelimiter.Designer.cs | 6 +- .../20200704081215_ImportProfileDelimiter.cs | 2 +- ..._ImportProfileDateNumberFormat.Designer.cs | 6 +- ...707141613_ImportProfileDateNumberFormat.cs | 2 +- .../20200723111131_BucketNotes.Designer.cs | 6 +- .../MySql}/20200723111131_BucketNotes.cs | 2 +- ...2402_AutomaticBucketAssignment.Designer.cs | 6 +- ...0200822152402_AutomaticBucketAssignment.cs | 2 +- .../MySql}/DatabaseServiceModelSnapshot.cs | 6 +- .../20200925091603_InitialCreate.Designer.cs | 275 ++++++ .../Sqlite/20200925091603_InitialCreate.cs | 211 +++++ ...200925091907_AddInitialRecords.Designer.cs | 275 ++++++ .../20200925091907_AddInitialRecords.cs | 33 + .../SqliteDatabaseContextModelSnapshot.cs | 273 ++++++ OpenBudgeteer.Core/Models/BucketVersion.cs | 4 +- OpenBudgeteer.Core/OpenBudgeteer.Core.csproj | 51 + .../OpenBudgeteer.Core.projitems | 45 - OpenBudgeteer.Core/OpenBudgeteer.Core.shproj | 13 - .../ViewModels/AccountViewModel.cs | 27 +- .../ViewModels/BucketViewModel.cs | 173 +++- .../ViewModels/ImportDataViewModel.cs | 241 +++-- .../ItemViewModels/AccountViewModelItem.cs | 63 +- .../BucketGroupViewModelItem.cs | 96 +- .../ItemViewModels/BucketViewModelItem.cs | 327 ++++--- .../MappingRuleViewModelItem.cs | 20 +- ...onthlyBucketExpensesReportViewModelItem.cs | 12 + .../PartialBucketViewModelItem.cs | 58 +- .../ItemViewModels/RuleSetViewModelItem.cs | 66 +- .../TransactionViewModelItem.cs | 224 +++-- .../ViewModels/ReportViewModel.cs | 52 +- .../ViewModels/RulesViewModel.cs | 107 ++- .../ViewModels/TransactionViewModel.cs | 103 +- .../ViewModels/YearMonthSelectorViewModel.cs | 60 +- OpenBudgeteer.sln | 18 +- 73 files changed, 3926 insertions(+), 629 deletions(-) create mode 100644 OpenBudgeteer.Core.Test/DbConnector.cs create mode 100644 OpenBudgeteer.Core.Test/OpenBudgeteer.Core.Test.csproj create mode 100644 OpenBudgeteer.Core.Test/ViewModelTest/AccountViewModelIsolatedTest.cs create mode 100644 OpenBudgeteer.Core.Test/ViewModelTest/AccountViewModelTest.cs create mode 100644 OpenBudgeteer.Core.Test/ViewModelTest/BucketViewModelIsolatedTest.cs create mode 100644 OpenBudgeteer.Core.Test/ViewModelTest/BucketViewModelTest.cs create mode 100644 OpenBudgeteer.Core.Test/ViewModelTest/YearMonthSelectorViewModelTest.cs rename OpenBudgeteer.Core/Common/{ => Database}/DatabaseContext.cs (99%) create mode 100644 OpenBudgeteer.Core/Common/Database/MySqlDatabaseContext.cs create mode 100644 OpenBudgeteer.Core/Common/Database/MySqlDatabaseContextFactory.cs create mode 100644 OpenBudgeteer.Core/Common/Database/SqliteDatabaseContext.cs create mode 100644 OpenBudgeteer.Core/Common/Database/SqliteDatabaseContextFactory.cs create mode 100644 OpenBudgeteer.Core/Common/ViewModelOperationResult.cs rename {OpenBudgeteer.Blazor/Migrations => OpenBudgeteer.Core/Migrations/MySql}/20200605093534_InitialCreate.Designer.cs (98%) rename {OpenBudgeteer.Blazor/Migrations => OpenBudgeteer.Core/Migrations/MySql}/20200605093534_InitialCreate.cs (99%) rename {OpenBudgeteer.Blazor/Migrations => OpenBudgeteer.Core/Migrations/MySql}/20200608152707_DecimalPrecision.Designer.cs (98%) rename {OpenBudgeteer.Blazor/Migrations => OpenBudgeteer.Core/Migrations/MySql}/20200608152707_DecimalPrecision.cs (98%) rename {OpenBudgeteer.Blazor/Migrations => OpenBudgeteer.Core/Migrations/MySql}/20200612082229_FixedBucketVersion.Designer.cs (98%) rename {OpenBudgeteer.Blazor/Migrations => OpenBudgeteer.Core/Migrations/MySql}/20200612082229_FixedBucketVersion.cs (92%) rename {OpenBudgeteer.Blazor/Migrations => OpenBudgeteer.Core/Migrations/MySql}/20200622121904_DefaultIncomeTransferBucket.Designer.cs (98%) rename {OpenBudgeteer.Blazor/Migrations => OpenBudgeteer.Core/Migrations/MySql}/20200622121904_DefaultIncomeTransferBucket.cs (96%) rename {OpenBudgeteer.Blazor/Migrations => OpenBudgeteer.Core/Migrations/MySql}/20200701071320_BucketColor.Designer.cs (98%) rename {OpenBudgeteer.Blazor/Migrations => OpenBudgeteer.Core/Migrations/MySql}/20200701071320_BucketColor.cs (92%) rename {OpenBudgeteer.Blazor/Migrations => OpenBudgeteer.Core/Migrations/MySql}/20200701133458_DateTimeDataType.Designer.cs (98%) rename {OpenBudgeteer.Blazor/Migrations => OpenBudgeteer.Core/Migrations/MySql}/20200701133458_DateTimeDataType.cs (98%) rename {OpenBudgeteer.Blazor/Migrations => OpenBudgeteer.Core/Migrations/MySql}/20200704081215_ImportProfileDelimiter.Designer.cs (98%) rename {OpenBudgeteer.Blazor/Migrations => OpenBudgeteer.Core/Migrations/MySql}/20200704081215_ImportProfileDelimiter.cs (95%) rename {OpenBudgeteer.Blazor/Migrations => OpenBudgeteer.Core/Migrations/MySql}/20200707141613_ImportProfileDateNumberFormat.Designer.cs (98%) rename {OpenBudgeteer.Blazor/Migrations => OpenBudgeteer.Core/Migrations/MySql}/20200707141613_ImportProfileDateNumberFormat.cs (95%) rename {OpenBudgeteer.Blazor/Migrations => OpenBudgeteer.Core/Migrations/MySql}/20200723111131_BucketNotes.Designer.cs (98%) rename {OpenBudgeteer.Blazor/Migrations => OpenBudgeteer.Core/Migrations/MySql}/20200723111131_BucketNotes.cs (92%) rename {OpenBudgeteer.Blazor/Migrations => OpenBudgeteer.Core/Migrations/MySql}/20200822152402_AutomaticBucketAssignment.Designer.cs (98%) rename {OpenBudgeteer.Blazor/Migrations => OpenBudgeteer.Core/Migrations/MySql}/20200822152402_AutomaticBucketAssignment.cs (97%) rename {OpenBudgeteer.Blazor/Migrations => OpenBudgeteer.Core/Migrations/MySql}/DatabaseServiceModelSnapshot.cs (98%) create mode 100644 OpenBudgeteer.Core/Migrations/Sqlite/20200925091603_InitialCreate.Designer.cs create mode 100644 OpenBudgeteer.Core/Migrations/Sqlite/20200925091603_InitialCreate.cs create mode 100644 OpenBudgeteer.Core/Migrations/Sqlite/20200925091907_AddInitialRecords.Designer.cs create mode 100644 OpenBudgeteer.Core/Migrations/Sqlite/20200925091907_AddInitialRecords.cs create mode 100644 OpenBudgeteer.Core/Migrations/Sqlite/SqliteDatabaseContextModelSnapshot.cs create mode 100644 OpenBudgeteer.Core/OpenBudgeteer.Core.csproj delete mode 100644 OpenBudgeteer.Core/OpenBudgeteer.Core.projitems delete mode 100644 OpenBudgeteer.Core/OpenBudgeteer.Core.shproj diff --git a/.gitignore b/.gitignore index 4ce6fdd..99229ad 100644 --- a/.gitignore +++ b/.gitignore @@ -3,6 +3,10 @@ ## ## Get latest from https://github.com/github/gitignore/blob/master/VisualStudio.gitignore +## Custom files +*.db +*.DS_Store + # User-specific files *.rsuser *.suo @@ -337,4 +341,4 @@ ASALocalRun/ .localhistory/ # BeatPulse healthcheck temp database -healthchecksdb \ No newline at end of file +healthchecksdb diff --git a/CHANGELOG.md b/CHANGELOG.md index 06e5b0c..3b9a6c9 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,8 @@ +### 1.3 (xxxx-xx-xx) + +* [Add] Support for Sqlite databases #2 +* [Add] Unit Tests (not full coverage yet) + ### 1.2.1 (2020-12-14) * [Fixed] Crash on Report Page due to wrong DateTime creation diff --git a/OpenBudgeteer.Blazor/Dockerfile b/OpenBudgeteer.Blazor/Dockerfile index 03e093a..025f14e 100644 --- a/OpenBudgeteer.Blazor/Dockerfile +++ b/OpenBudgeteer.Blazor/Dockerfile @@ -8,6 +8,7 @@ EXPOSE 443 FROM mcr.microsoft.com/dotnet/core/sdk:3.1-buster AS build WORKDIR /src COPY ["OpenBudgeteer.Blazor/OpenBudgeteer.Blazor.csproj", "OpenBudgeteer.Blazor/"] +COPY ["OpenBudgeteer.Core/OpenBudgeteer.Core.csproj", "OpenBudgeteer.Core/"] RUN dotnet restore "OpenBudgeteer.Blazor/OpenBudgeteer.Blazor.csproj" COPY . . WORKDIR "/src/OpenBudgeteer.Blazor" @@ -19,4 +20,5 @@ RUN dotnet publish "OpenBudgeteer.Blazor.csproj" -c Release -o /app/publish FROM base AS final WORKDIR /app COPY --from=publish /app/publish . +RUN mkdir database ENTRYPOINT ["dotnet", "OpenBudgeteer.dll"] \ No newline at end of file diff --git a/OpenBudgeteer.Blazor/OpenBudgeteer.Blazor.csproj b/OpenBudgeteer.Blazor/OpenBudgeteer.Blazor.csproj index 5cc9e1b..9755817 100644 --- a/OpenBudgeteer.Blazor/OpenBudgeteer.Blazor.csproj +++ b/OpenBudgeteer.Blazor/OpenBudgeteer.Blazor.csproj @@ -7,38 +7,21 @@ OpenBudgeteer - - - - - - - - - - - - - - - - - - - + all runtime; build; native; contentfiles; analyzers; buildtransitive - - + + - - + + + diff --git a/OpenBudgeteer.Blazor/Pages/Account.razor b/OpenBudgeteer.Blazor/Pages/Account.razor index 551a76a..b26d108 100644 --- a/OpenBudgeteer.Blazor/Pages/Account.razor +++ b/OpenBudgeteer.Blazor/Pages/Account.razor @@ -1,10 +1,11 @@ @page "/account" @using OpenBudgeteer.Core.ViewModels -@using OpenBudgeteer.Core.Common +@using OpenBudgeteer.Core.Common.Database @using Microsoft.EntityFrameworkCore @using OpenBudgeteer.Core.ViewModels.ItemViewModels @using System.Globalization +@using OpenBudgeteer.Core.Common @inject DbContextOptions DbContextOptions
@@ -74,7 +75,7 @@ _dataContext.LoadData(); StateHasChanged(); }; - HandleResult(new Tuple(true, "")); + HandleResult(new ViewModelOperationResult(true)); } private void CreateNewAccount() @@ -109,12 +110,11 @@ HandleResult(account.CloseAccount()); } - void HandleResult(Tuple result) + void HandleResult(ViewModelOperationResult result) { - var (success, errorMessage) = result; - if (!success) + if (!result.IsSuccessful) { - _errorModalDialogMessage = errorMessage; + _errorModalDialogMessage = result.Message; _isErrorModalDialogVisible = true; } } diff --git a/OpenBudgeteer.Blazor/Pages/Bucket.razor b/OpenBudgeteer.Blazor/Pages/Bucket.razor index a975ee9..24967f4 100644 --- a/OpenBudgeteer.Blazor/Pages/Bucket.razor +++ b/OpenBudgeteer.Blazor/Pages/Bucket.razor @@ -1,10 +1,11 @@ @page "/bucket" -@using OpenBudgeteer.Core.Common +@using OpenBudgeteer.Core.Common.Database @using OpenBudgeteer.Core.ViewModels @using OpenBudgeteer.Core.ViewModels.ItemViewModels @using Microsoft.EntityFrameworkCore @using System.Drawing @using System.Globalization +@using OpenBudgeteer.Core.Common @inject DbContextOptions DbContextOptions @inject YearMonthSelectorViewModel YearMonthDataContext @@ -397,12 +398,11 @@ StateHasChanged(); } - void HandleResult(Tuple result) + void HandleResult(ViewModelOperationResult result) { - var (success, errorMessage) = result; - if (!success) + if (!result.IsSuccessful) { - _errorModalDialogMessage = errorMessage; + _errorModalDialogMessage = result.Message; _isErrorModalDialogVisible = true; } } @@ -410,7 +410,7 @@ void InOut_Changed(BucketViewModelItem bucket, KeyboardEventArgs args) { var result = bucket.HandleInOutInput(args.Key); - if (result.Item1) + if (result.IsSuccessful) { HandleResult(_dataContext.UpdateBalanceFigures()); StateHasChanged(); diff --git a/OpenBudgeteer.Blazor/Pages/Import.razor b/OpenBudgeteer.Blazor/Pages/Import.razor index d02bea7..a677eeb 100644 --- a/OpenBudgeteer.Blazor/Pages/Import.razor +++ b/OpenBudgeteer.Blazor/Pages/Import.razor @@ -1,10 +1,11 @@ @page "/import" @using OpenBudgeteer.Core.ViewModels -@using OpenBudgeteer.Core.Common +@using OpenBudgeteer.Core.Common.Database @using Microsoft.EntityFrameworkCore @using System.IO @using System.Text +@using OpenBudgeteer.Core.Common @using OpenBudgeteer.Core.Models @using Tewr.Blazor.FileReader @inject DbContextOptions DbContextOptions @@ -362,7 +363,7 @@ _dataContext.SelectedImportProfile = _dataContext.AvailableImportProfiles.First(); } var result = _dataContext.LoadProfile(); - if (result.Item1) + if (result.IsSuccessful) { _step3Enabled = _dataContext.SelectedImportProfile.ImportProfileId != 0; CheckColumnMapping(); @@ -377,7 +378,7 @@ void LoadHeaders() { var result = _dataContext.LoadHeaders(); - if (result.Item1) + if (result.IsSuccessful) { _step3Enabled = true; } @@ -404,13 +405,13 @@ void ValidateData() { - _validationErrorMessage = _dataContext.ValidateData(); + _validationErrorMessage = _dataContext.ValidateData().Message; } void ImportData() { var result = _dataContext.ImportData(); - _importConfirmationMessage = result.Item2; + _importConfirmationMessage = result.Message; _isConfirmationModalDialogVisible = true; } @@ -457,12 +458,11 @@ CheckColumnMapping(); } - void HandleResult(Tuple result) + void HandleResult(ViewModelOperationResult result) { - var (success, errorMessage) = result; - if (!success) + if (!result.IsSuccessful) { - _errorModalDialogMessage = errorMessage; + _errorModalDialogMessage = result.Message; _isErrorModalDialogVisible = true; } } diff --git a/OpenBudgeteer.Blazor/Pages/Report.razor b/OpenBudgeteer.Blazor/Pages/Report.razor index bdfc702..9b8f833 100644 --- a/OpenBudgeteer.Blazor/Pages/Report.razor +++ b/OpenBudgeteer.Blazor/Pages/Report.razor @@ -9,7 +9,7 @@ @using ChartJs.Blazor.ChartJS.Common.Properties @using ChartJs.Blazor.Util @using Microsoft.EntityFrameworkCore -@using OpenBudgeteer.Core.Common +@using OpenBudgeteer.Core.Common.Database @using OpenBudgeteer.Blazor.ViewModels @inject DbContextOptions DbContextOptions diff --git a/OpenBudgeteer.Blazor/Pages/Rules.razor b/OpenBudgeteer.Blazor/Pages/Rules.razor index 80a0349..1d9febd 100644 --- a/OpenBudgeteer.Blazor/Pages/Rules.razor +++ b/OpenBudgeteer.Blazor/Pages/Rules.razor @@ -1,10 +1,11 @@ @page "/rules" @using Microsoft.EntityFrameworkCore -@using OpenBudgeteer.Core.Common +@using OpenBudgeteer.Core.Common.Database @using OpenBudgeteer.Core.ViewModels @using OpenBudgeteer.Core.ViewModels.ItemViewModels @using System.Drawing +@using OpenBudgeteer.Core.Common @using OpenBudgeteer.Core.Models @inject DbContextOptions DbContextOptions @@ -305,12 +306,11 @@ HandleResult(_dataContext.DeleteRuleSetItem(_ruleSetToBeDeleted)); } - void HandleResult(Tuple result) + void HandleResult(ViewModelOperationResult result) { - var (success, errorMessage) = result; - if (!success) + if (!result.IsSuccessful) { - _errorModalDialogMessage = errorMessage; + _errorModalDialogMessage = result.Message; _isErrorModalDialogVisible = true; } } diff --git a/OpenBudgeteer.Blazor/Pages/Transaction.razor b/OpenBudgeteer.Blazor/Pages/Transaction.razor index ec6a3a2..a14e089 100644 --- a/OpenBudgeteer.Blazor/Pages/Transaction.razor +++ b/OpenBudgeteer.Blazor/Pages/Transaction.razor @@ -1,9 +1,10 @@ @page "/transaction" @using OpenBudgeteer.Core.ViewModels -@using OpenBudgeteer.Core.Common +@using OpenBudgeteer.Core.Common.Database @using Microsoft.EntityFrameworkCore @using System.Drawing +@using OpenBudgeteer.Core.Common @using OpenBudgeteer.Core.ViewModels.ItemViewModels @inject DbContextOptions DbContextOptions @inject YearMonthSelectorViewModel YearMonthDataContext @@ -292,12 +293,11 @@ HandleResult(_transactionToBeDeleted.DeleteItem()); } - void HandleResult(Tuple result) + void HandleResult(ViewModelOperationResult result) { - var (success, errorMessage) = result; - if (!success) + if (!result.IsSuccessful) { - _errorModalDialogMessage = errorMessage; + _errorModalDialogMessage = result.Message; _isErrorModalDialogVisible = true; } } diff --git a/OpenBudgeteer.Blazor/Shared/NavMenu.razor b/OpenBudgeteer.Blazor/Shared/NavMenu.razor index fc88744..9656122 100644 --- a/OpenBudgeteer.Blazor/Shared/NavMenu.razor +++ b/OpenBudgeteer.Blazor/Shared/NavMenu.razor @@ -1,4 +1,4 @@ -@using OpenBudgeteer.Core.Common +@using OpenBudgeteer.Core.Common.Database @using Microsoft.EntityFrameworkCore @inject DbContextOptions DbContextOptions diff --git a/OpenBudgeteer.Blazor/Startup.cs b/OpenBudgeteer.Blazor/Startup.cs index f25753f..ef2363d 100644 --- a/OpenBudgeteer.Blazor/Startup.cs +++ b/OpenBudgeteer.Blazor/Startup.cs @@ -12,7 +12,7 @@ using Microsoft.Extensions.Configuration; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Hosting; -using OpenBudgeteer.Core.Common; +using OpenBudgeteer.Core.Common.Database; using OpenBudgeteer.Core.ViewModels; using Tewr.Blazor.FileReader; @@ -36,20 +36,52 @@ public void ConfigureServices(IServiceCollection services) services.AddFileReaderService(); services.AddScoped(); var configurationSection = Configuration.GetSection("Connection"); - var connectionString = $"Server={configurationSection?["Server"]};" + + var provider = configurationSection?["Provider"]; + string connectionString; + switch (provider) + { + case "mysql": + connectionString = $"Server={configurationSection?["Server"]};" + $"Port={configurationSection?["Port"]};" + $"Database={configurationSection?["Database"]};" + $"User={configurationSection?["User"]};" + $"Password={configurationSection?["Password"]}"; - services.AddDbContext(options => options.UseMySql( - connectionString, - b => b.MigrationsAssembly("OpenBudgeteer")), - ServiceLifetime.Transient); + + services.AddDbContext(options => options.UseMySql( + connectionString, + b => b.MigrationsAssembly("OpenBudgeteer.Core")), + ServiceLifetime.Transient); + + // Check on Pending Db Migrations + var mySqlDbContext = new MySqlDatabaseContextFactory().CreateDbContext(Configuration); + if (mySqlDbContext.Database.GetPendingMigrations().Any()) mySqlDbContext.Database.Migrate(); + + break; + case "sqlite": +#if DEBUG + connectionString = "Data Source=openbudgeteer.db"; +#else + connectionString = "Data Source=database/openbudgeteer.db"; +#endif + services.AddDbContext(options => options.UseSqlite( + connectionString, + b => b.MigrationsAssembly("OpenBudgeteer.Core")), + ServiceLifetime.Transient); + + // Check on Pending Db Migrations + var sqliteDbContext = new SqliteDatabaseContextFactory().CreateDbContext(connectionString); + if (sqliteDbContext.Database.GetPendingMigrations().Any()) sqliteDbContext.Database.Migrate(); + + break; + default: + throw new ArgumentOutOfRangeException($"Database provider {provider} not supported"); + } + Encoding.RegisterProvider(CodePagesEncodingProvider.Instance); // Required to read ANSI Text files } // This method gets called by the runtime. Use this method to configure the HTTP request pipeline. - public void Configure(IApplicationBuilder app, IWebHostEnvironment env, DatabaseContext dbContext) + public void Configure(IApplicationBuilder app, IWebHostEnvironment env) { if (env.IsDevelopment()) { @@ -73,7 +105,6 @@ public void Configure(IApplicationBuilder app, IWebHostEnvironment env, Database endpoints.MapFallbackToPage("/_Host"); }); - if (dbContext.Database.GetPendingMigrations().Any()) dbContext.Database.Migrate(); // TODO Get Culture from Settings var cultureInfo = new CultureInfo("de-DE"); CultureInfo.DefaultThreadCurrentCulture = cultureInfo; diff --git a/OpenBudgeteer.Blazor/ViewModels/BlazorReportViewModel.cs b/OpenBudgeteer.Blazor/ViewModels/BlazorReportViewModel.cs index f0d2633..2c29efe 100644 --- a/OpenBudgeteer.Blazor/ViewModels/BlazorReportViewModel.cs +++ b/OpenBudgeteer.Blazor/ViewModels/BlazorReportViewModel.cs @@ -14,7 +14,7 @@ using ChartJs.Blazor.ChartJS.LineChart; using ChartJs.Blazor.Util; using Microsoft.EntityFrameworkCore; -using OpenBudgeteer.Core.Common; +using OpenBudgeteer.Core.Common.Database; using OpenBudgeteer.Core.ViewModels; namespace OpenBudgeteer.Blazor.ViewModels diff --git a/OpenBudgeteer.Blazor/appsettings.json b/OpenBudgeteer.Blazor/appsettings.json index 1dfa011..1c937d5 100644 --- a/OpenBudgeteer.Blazor/appsettings.json +++ b/OpenBudgeteer.Blazor/appsettings.json @@ -1,10 +1,11 @@ { "Connection": { + "Provider" : "mysql", "Database": "openbudgeteer-dev", "Server": "cl4p-tp", "Port": "3306", "User": "openbudgeteer-dev", - "Password": "openbudgeteer-dev" + "Password": "openbudgeteer-dev" }, "Logging": { "LogLevel": { diff --git a/OpenBudgeteer.Core.Test/DbConnector.cs b/OpenBudgeteer.Core.Test/DbConnector.cs new file mode 100644 index 0000000..dd7bd95 --- /dev/null +++ b/OpenBudgeteer.Core.Test/DbConnector.cs @@ -0,0 +1,49 @@ +using System.Linq; +using Microsoft.EntityFrameworkCore; +using OpenBudgeteer.Core.Common.Database; + +namespace OpenBudgeteer.Core.Test +{ + public class DbConnector + { + public static DbContextOptions GetDbContextOptions(string dbName) + { + //var connectionString = "Server=cl4p-tp;" + + // "Port=3306;" + + // "Database=openbudgeteer-test;" + + // "User=openbudgeteer-test;" + + // "Password=openbudgeteer-test"; + //return new DbContextOptionsBuilder() + // .UseMySql(connectionString) + // .Options; + + var connectionString = $"Data Source={dbName}.db"; + + //Check on Pending Db Migrations + var sqliteDbContext = new SqliteDatabaseContextFactory().CreateDbContext(connectionString); + if (sqliteDbContext.Database.GetPendingMigrations().Any()) + sqliteDbContext.Database.Migrate(); + + return new DbContextOptionsBuilder() + .UseSqlite(connectionString) + .Options; + } + + public static void CleanupDatabase(string dbName) + { + using (var dbContext = new DatabaseContext(GetDbContextOptions(dbName))) + { + dbContext.DeleteAccounts(dbContext.Account); + dbContext.DeleteBankTransactions(dbContext.BankTransaction); + dbContext.DeleteBuckets(dbContext.Bucket); + dbContext.DeleteBucketGroups(dbContext.BucketGroup); + dbContext.DeleteBucketMovements(dbContext.BucketMovement); + dbContext.DeleteBucketRuleSets(dbContext.BucketRuleSet); + dbContext.DeleteBucketVersions(dbContext.BucketVersion); + dbContext.DeleteBudgetedTransactions(dbContext.BudgetedTransaction); + dbContext.DeleteImportProfiles(dbContext.ImportProfile); + dbContext.DeleteMappingRules(dbContext.MappingRule); + } + } + } +} diff --git a/OpenBudgeteer.Core.Test/OpenBudgeteer.Core.Test.csproj b/OpenBudgeteer.Core.Test/OpenBudgeteer.Core.Test.csproj new file mode 100644 index 0000000..ae4a64b --- /dev/null +++ b/OpenBudgeteer.Core.Test/OpenBudgeteer.Core.Test.csproj @@ -0,0 +1,24 @@ + + + + netcoreapp3.1 + + false + + + + + + + + + + + + + + + + + + diff --git a/OpenBudgeteer.Core.Test/ViewModelTest/AccountViewModelIsolatedTest.cs b/OpenBudgeteer.Core.Test/ViewModelTest/AccountViewModelIsolatedTest.cs new file mode 100644 index 0000000..000a66a --- /dev/null +++ b/OpenBudgeteer.Core.Test/ViewModelTest/AccountViewModelIsolatedTest.cs @@ -0,0 +1,47 @@ +using System.Linq; +using Microsoft.EntityFrameworkCore; +using OpenBudgeteer.Core.Common.Database; +using OpenBudgeteer.Core.Models; +using OpenBudgeteer.Core.ViewModels; +using Xunit; + +namespace OpenBudgeteer.Core.Test.ViewModelTest +{ + [CollectionDefinition("AccountViewModelIsolatedTest", DisableParallelization = true)] + public class AccountViewModelIsolatedTest + { + private readonly DbContextOptions _dbOptions; + + public AccountViewModelIsolatedTest() + { + _dbOptions = DbConnector.GetDbContextOptions(nameof(AccountViewModelIsolatedTest)); + } + + [Fact] + public void LoadData_CheckNameAndLoadOnlyActiveAccounts() + { + DbConnector.CleanupDatabase(nameof(AccountViewModelIsolatedTest)); + + using (var dbContext = new DatabaseContext(_dbOptions)) + { + dbContext.CreateAccounts(new[] + { + new Account() {Name = "Test Account1", IsActive = 1}, + new Account() {Name = "Test Account2", IsActive = 1}, + new Account() {Name = "Test Account3", IsActive = 0} + }); + } + + var viewModel = new AccountViewModel(_dbOptions); + viewModel.LoadData(); + + Assert.Equal(2, viewModel.Accounts.Count); + + var testItem1 = viewModel.Accounts.ElementAt(0); + var testItem2 = viewModel.Accounts.ElementAt(1); + + Assert.Equal("Test Account1", testItem1.Account.Name); + Assert.Equal("Test Account2", testItem2.Account.Name); + } + } +} \ No newline at end of file diff --git a/OpenBudgeteer.Core.Test/ViewModelTest/AccountViewModelTest.cs b/OpenBudgeteer.Core.Test/ViewModelTest/AccountViewModelTest.cs new file mode 100644 index 0000000..98e485e --- /dev/null +++ b/OpenBudgeteer.Core.Test/ViewModelTest/AccountViewModelTest.cs @@ -0,0 +1,71 @@ +using System; +using System.Collections.Generic; +using System.Diagnostics.Contracts; +using System.Linq; +using Microsoft.EntityFrameworkCore; +using OpenBudgeteer.Core.Common.Database; +using OpenBudgeteer.Core.Models; +using OpenBudgeteer.Core.ViewModels; +using Xunit; + +namespace OpenBudgeteer.Core.Test.ViewModelTest +{ + public class AccountViewModelTest + { + private readonly DbContextOptions _dbOptions; + + public AccountViewModelTest() + { + _dbOptions = DbConnector.GetDbContextOptions(nameof(AccountViewModelTest)); + } + + public static IEnumerable TestData_LoadData_CheckTransactionCalculations + { + get + { + return new[] + { + new object[] {new List {12.34m, -12.34m, 12.34m}, 12.34m, 24.68m, -12.34m}, + new object[] {new List {0}, 0, 0, 0} + }; + } + } + + [Theory] + [MemberData(nameof(TestData_LoadData_CheckTransactionCalculations))] + public void LoadData_CheckTransactionCalculations( + List transactionAmounts, + decimal expectedBalance, + decimal expectedIn, + decimal expectedOut) + { + using (var dbContext = new DatabaseContext(_dbOptions)) + { + var testAccount = new Account() {Name = "Test Account", IsActive = 1}; + dbContext.CreateAccount(testAccount); + + foreach (var transactionAmount in transactionAmounts) + { + dbContext.CreateBankTransaction( + new BankTransaction() + { + AccountId = testAccount.AccountId, + TransactionDate = DateTime.Now, + Amount = transactionAmount + } + ); + } + + var viewModel = new AccountViewModel(_dbOptions); + viewModel.LoadData(); + var testItem1 = viewModel.Accounts + .FirstOrDefault(i => i.Account.AccountId == testAccount.AccountId); + + Assert.NotNull(testItem1); + Assert.Equal(expectedBalance, testItem1.Balance); + Assert.Equal(expectedIn, testItem1.In); + Assert.Equal(expectedOut, testItem1.Out); + } + } + } +} diff --git a/OpenBudgeteer.Core.Test/ViewModelTest/BucketViewModelIsolatedTest.cs b/OpenBudgeteer.Core.Test/ViewModelTest/BucketViewModelIsolatedTest.cs new file mode 100644 index 0000000..3b7c72e --- /dev/null +++ b/OpenBudgeteer.Core.Test/ViewModelTest/BucketViewModelIsolatedTest.cs @@ -0,0 +1,142 @@ +using System.Linq; +using System.Threading.Tasks; +using Microsoft.EntityFrameworkCore; +using OpenBudgeteer.Core.Common.Database; +using OpenBudgeteer.Core.Models; +using OpenBudgeteer.Core.ViewModels; +using Xunit; + +namespace OpenBudgeteer.Core.Test.ViewModelTest +{ + [CollectionDefinition("BucketViewModelIsolatedTest", DisableParallelization = true)] + public class BucketViewModelIsolatedTest + { + private readonly DbContextOptions _dbOptions; + + public BucketViewModelIsolatedTest() + { + _dbOptions = DbConnector.GetDbContextOptions(nameof(BucketViewModelIsolatedTest)); + } + + [Fact] + public async Task LoadDataAsync_CheckBucketGroupsNamesAndPositions() + { + DbConnector.CleanupDatabase(nameof(BucketViewModelIsolatedTest)); + + using (var dbContext = new DatabaseContext(_dbOptions)) + { + dbContext.CreateBucketGroups(new[] + { + new BucketGroup() { Name = "Bucket Group 1", Position = 1}, + new BucketGroup() { Name = "Bucket Group 2", Position = 3}, + new BucketGroup() { Name = "Bucket Group 3", Position = 2} + }); + } + + var monthSelectorViewModel = new YearMonthSelectorViewModel(); + var viewModel = new BucketViewModel(_dbOptions, monthSelectorViewModel); + await viewModel.LoadDataAsync(); + + Assert.Equal(3, viewModel.BucketGroups.Count); + Assert.Equal("Bucket Group 1", viewModel.BucketGroups.ElementAt(0).BucketGroup.Name); + Assert.Equal("Bucket Group 3", viewModel.BucketGroups.ElementAt(1).BucketGroup.Name); + Assert.Equal("Bucket Group 2", viewModel.BucketGroups.ElementAt(2).BucketGroup.Name); + + Assert.Equal(1, viewModel.BucketGroups.ElementAt(0).BucketGroup.Position); + Assert.Equal(2, viewModel.BucketGroups.ElementAt(1).BucketGroup.Position); + Assert.Equal(3, viewModel.BucketGroups.ElementAt(2).BucketGroup.Position); + } + + [Fact] + public async Task CreateGroup_CheckGroupCreationAndPositions() + { + DbConnector.CleanupDatabase(nameof(BucketViewModelIsolatedTest)); + + using (var dbContext = new DatabaseContext(_dbOptions)) + { + dbContext.CreateBucketGroups(new[] + { + new BucketGroup() { Name = "Bucket Group 1", Position = 1 }, + new BucketGroup() { Name = "Bucket Group 2", Position = 3 }, + new BucketGroup() { Name = "Bucket Group 3", Position = 2 } + }); + } + + var monthSelectorViewModel = new YearMonthSelectorViewModel(); + var viewModel = new BucketViewModel(_dbOptions, monthSelectorViewModel); + await viewModel.LoadDataAsync(); + + var result = viewModel.CreateGroup(); + + Assert.True(result.IsSuccessful); + Assert.Equal(4, viewModel.BucketGroups.Count); + Assert.Equal("New Bucket Group", viewModel.BucketGroups.ElementAt(0).BucketGroup.Name); + Assert.Equal("Bucket Group 1", viewModel.BucketGroups.ElementAt(1).BucketGroup.Name); + Assert.Equal("Bucket Group 3", viewModel.BucketGroups.ElementAt(2).BucketGroup.Name); + Assert.Equal("Bucket Group 2", viewModel.BucketGroups.ElementAt(3).BucketGroup.Name); + Assert.Equal(1, viewModel.BucketGroups.ElementAt(0).BucketGroup.Position); + Assert.Equal(2, viewModel.BucketGroups.ElementAt(1).BucketGroup.Position); + Assert.Equal(3, viewModel.BucketGroups.ElementAt(2).BucketGroup.Position); + Assert.Equal(4, viewModel.BucketGroups.ElementAt(3).BucketGroup.Position); + Assert.True(viewModel.BucketGroups.First().InModification); + + // Reload ViewModel to see if changes has been also reflected onto the database + await viewModel.LoadDataAsync(); + + Assert.True(result.IsSuccessful); + Assert.Equal(4, viewModel.BucketGroups.Count); + Assert.Equal("New Bucket Group", viewModel.BucketGroups.ElementAt(0).BucketGroup.Name); + Assert.Equal("Bucket Group 1", viewModel.BucketGroups.ElementAt(1).BucketGroup.Name); + Assert.Equal("Bucket Group 3", viewModel.BucketGroups.ElementAt(2).BucketGroup.Name); + Assert.Equal("Bucket Group 2", viewModel.BucketGroups.ElementAt(3).BucketGroup.Name); + Assert.Equal(1, viewModel.BucketGroups.ElementAt(0).BucketGroup.Position); + Assert.Equal(2, viewModel.BucketGroups.ElementAt(1).BucketGroup.Position); + Assert.Equal(3, viewModel.BucketGroups.ElementAt(2).BucketGroup.Position); + Assert.Equal(4, viewModel.BucketGroups.ElementAt(3).BucketGroup.Position); + Assert.False(viewModel.BucketGroups.First().InModification); + } + + [Fact] + public async Task DeleteGroup_CheckGroupDeletionAndPositions() + { + DbConnector.CleanupDatabase(nameof(BucketViewModelIsolatedTest)); + + using (var dbContext = new DatabaseContext(_dbOptions)) + { + dbContext.CreateBucketGroups(new[] + { + new BucketGroup() { Name = "Bucket Group 1", Position = 1}, + new BucketGroup() { Name = "Bucket Group 2", Position = 3}, + new BucketGroup() { Name = "Bucket Group 3", Position = 2} + }); + } + + var monthSelectorViewModel = new YearMonthSelectorViewModel(); + var viewModel = new BucketViewModel(_dbOptions, monthSelectorViewModel); + await viewModel.LoadDataAsync(); + + var groupToDelete = viewModel.BucketGroups.ElementAt(1); + var result = viewModel.DeleteGroup(groupToDelete); + + Assert.True(result.IsSuccessful); + Assert.True(result.ViewModelReloadInvoked); + + await viewModel.LoadDataAsync(); + + Assert.Equal(2, viewModel.BucketGroups.Count); + Assert.Equal("Bucket Group 1", viewModel.BucketGroups.ElementAt(0).BucketGroup.Name); + Assert.Equal("Bucket Group 2", viewModel.BucketGroups.ElementAt(1).BucketGroup.Name); + Assert.Equal(1, viewModel.BucketGroups.ElementAt(0).BucketGroup.Position); + Assert.Equal(2, viewModel.BucketGroups.ElementAt(1).BucketGroup.Position); + + // Reload ViewModel to see if changes has been also reflected onto the database + await viewModel.LoadDataAsync(); + + Assert.Equal(2, viewModel.BucketGroups.Count); + Assert.Equal("Bucket Group 1", viewModel.BucketGroups.ElementAt(0).BucketGroup.Name); + Assert.Equal("Bucket Group 2", viewModel.BucketGroups.ElementAt(1).BucketGroup.Name); + Assert.Equal(1, viewModel.BucketGroups.ElementAt(0).BucketGroup.Position); + Assert.Equal(2, viewModel.BucketGroups.ElementAt(1).BucketGroup.Position); + } + } +} \ No newline at end of file diff --git a/OpenBudgeteer.Core.Test/ViewModelTest/BucketViewModelTest.cs b/OpenBudgeteer.Core.Test/ViewModelTest/BucketViewModelTest.cs new file mode 100644 index 0000000..de7ce3c --- /dev/null +++ b/OpenBudgeteer.Core.Test/ViewModelTest/BucketViewModelTest.cs @@ -0,0 +1,895 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; +using Microsoft.EntityFrameworkCore; +using OpenBudgeteer.Core.Common.Database; +using OpenBudgeteer.Core.Models; +using OpenBudgeteer.Core.ViewModels; +using OpenBudgeteer.Core.ViewModels.ItemViewModels; +using Xunit; + +namespace OpenBudgeteer.Core.Test.ViewModelTest +{ + public class BucketViewModelTest + { + private readonly DbContextOptions _dbOptions; + + public BucketViewModelTest() + { + _dbOptions = DbConnector.GetDbContextOptions(nameof(BucketViewModelTest)); + } + + public static IEnumerable TestData_LoadDataAsync_CheckBucketGroupAssignedBuckets + { + get + { + return new[] + { + new object[] {new List {"Bucket 1"}}, + new object[] {new List {"Bucket 1", "Bucket 2", "Bucket 3"}}, + new object[] {new List()}, + }; + } + } + + [Theory] + [MemberData(nameof(TestData_LoadDataAsync_CheckBucketGroupAssignedBuckets))] + public async Task LoadDataAsync_CheckBucketGroupAssignedBuckets(List bucketNames) + { + using (var dbContext = new DatabaseContext(_dbOptions)) + { + var testBucketGroup = new BucketGroup() { Name = "Bucket Group", Position = 1 }; + dbContext.CreateBucketGroup(testBucketGroup); + + foreach (var bucketName in bucketNames) + { + dbContext.CreateBucket(new Bucket() + { + BucketGroupId = testBucketGroup.BucketGroupId, + Name = bucketName, + ColorCode = "Red", + ValidFrom = new DateTime(2010, 1, 1) + }); + } + + var monthSelectorViewModel = new YearMonthSelectorViewModel(); + var viewModel = new BucketViewModel(_dbOptions, monthSelectorViewModel); + await viewModel.LoadDataAsync(); + + var testObject = viewModel.BucketGroups + .FirstOrDefault(i => i.BucketGroup.BucketGroupId == testBucketGroup.BucketGroupId); + + Assert.NotNull(testObject); + Assert.Equal(bucketNames.Count, testObject.Buckets.Count); + foreach (var bucketName in bucketNames) + { + Assert.Contains(testObject.Buckets, i => i.Bucket.Name == bucketName); + } + } + } + + public static IEnumerable TestData_LoadDataAsync_CheckBucketSorting + { + get + { + return new[] + { + new object[] {new List {"A_Bucket 1", "C_Bucket 2", "B_Bucket 3"}, new List {"A_Bucket 1", "B_Bucket 3", "C_Bucket 2"} } + }; + } + } + + [Theory] + [MemberData(nameof(TestData_LoadDataAsync_CheckBucketSorting))] + public async Task LoadDataAsync_CheckBucketSorting(List bucketNamesUnsorted, List expectedBucketNamesSorted) + { + using (var dbContext = new DatabaseContext(_dbOptions)) + { + var testBucketGroup = new BucketGroup() { Name = "Bucket Group", Position = 1 }; + dbContext.CreateBucketGroup(testBucketGroup); + + foreach (var bucketName in bucketNamesUnsorted) + { + dbContext.CreateBucket(new Bucket() + { + BucketGroupId = testBucketGroup.BucketGroupId, + Name = bucketName, + ColorCode = "Red", + ValidFrom = new DateTime(2010, 1, 1) + }); + } + + var monthSelectorViewModel = new YearMonthSelectorViewModel(); + var viewModel = new BucketViewModel(_dbOptions, monthSelectorViewModel); + await viewModel.LoadDataAsync(); + + var bucketGroup = viewModel.BucketGroups.FirstOrDefault(i => i.BucketGroup.BucketGroupId == testBucketGroup.BucketGroupId); + Assert.NotNull(bucketGroup); + Assert.Equal(bucketNamesUnsorted.Count, bucketGroup.Buckets.Count); + + for (int i = 0; i < bucketGroup.Buckets.Count; i++) + { + Assert.Equal( + expectedBucketNamesSorted.ElementAt(i), + bucketGroup.Buckets.ElementAt(i).Bucket.Name); + } + } + } + + public static IEnumerable TestData_LoadDataAsync_LoadOnlyActiveBuckets + { + get + { + return new[] + { + // Active in current month + new object[] { new DateTime(2010,1,1), new DateTime(2010,1,1), false, null, true}, + // Active starting next month + new object[] { new DateTime(2010,1,1), new DateTime(2010,2,1), false, null, false}, + // Active starting next year + new object[] { new DateTime(2010,1,1), new DateTime(2011,1,1), false, null, false}, + // Inactive since current month + new object[] { new DateTime(2010,1,1), new DateTime(2009,1,1), true, new DateTime(2010,1,1), false}, + // Inactive since last year + new object[] { new DateTime(2010,1,1), new DateTime(2009,1,1), true, new DateTime(2009,1,1), false}, + // Inactive since last month + new object[] { new DateTime(2010,2,1), new DateTime(2009,1,1), true, new DateTime(2010,1,1), false}, + // Inactive starting next month + new object[] { new DateTime(2010,1,1), new DateTime(2010,1,1), true, new DateTime(2010,2,1), true}, + // Active starting next month but already inactive in the future + new object[] { new DateTime(2010,1,1), new DateTime(2010,2,1), true, new DateTime(2010,3,1), false} + }; + } + } + + [Theory] + [MemberData(nameof(TestData_LoadDataAsync_LoadOnlyActiveBuckets))] + public async Task LoadDataAsync_LoadOnlyActiveBuckets( + DateTime testMonth, + DateTime bucketActiveSince, + bool bucketIsInactive, + DateTime bucketIsInActiveFrom, + bool expectedBucketAvailable + ) + { + using (var dbContext = new DatabaseContext(_dbOptions)) + { + var testAccount = new Account() {IsActive = 1, Name = "Account"}; + var testBucketGroup = new BucketGroup() {Name = "Bucket Group", Position = 1}; + + dbContext.CreateAccount(testAccount); + dbContext.CreateBucketGroup(testBucketGroup); + + var testBucket = new Bucket() + { + BucketGroupId = testBucketGroup.BucketGroupId, + Name = "Bucket", + ValidFrom = bucketActiveSince, + IsInactive = bucketIsInactive, + IsInactiveFrom = bucketIsInActiveFrom, + }; + + dbContext.CreateBucket(testBucket); + dbContext.CreateBucketVersion(new BucketVersion() + { + BucketId = testBucket.BucketId, + Version = 1, + BucketType = 1, + ValidFrom = bucketActiveSince + }); + + var monthSelectorViewModel = new YearMonthSelectorViewModel() + { + SelectedYear = testMonth.Year, + SelectedMonth = testMonth.Month + }; + var viewModel = new BucketViewModel(_dbOptions, monthSelectorViewModel); + await viewModel.LoadDataAsync(); + + var bucketGroup = viewModel.BucketGroups.FirstOrDefault(i => i.BucketGroup.BucketGroupId == testBucketGroup.BucketGroupId); + Assert.NotNull(bucketGroup); + + Assert.Equal(expectedBucketAvailable, bucketGroup.Buckets.Any()); + } + } + + [Fact] + public async Task LoadDataAsync_CheckValidFromHandling() + { + try + { + using (var dbContext = new DatabaseContext(_dbOptions)) + { + var testAccount = new Account() { IsActive = 1, Name = "Account" }; + var testBucketGroup = new BucketGroup() { Name = "Bucket Group", Position = 1 }; + + dbContext.CreateAccount(testAccount); + dbContext.CreateBucketGroup(testBucketGroup); + + var testBucket1 = new Bucket() + { + BucketGroupId = testBucketGroup.BucketGroupId, + Name = "Bucket Active Current Month", + ValidFrom = new DateTime(2010, 1, 1) + }; + var testBucket2 = new Bucket() + { + BucketGroupId = testBucketGroup.BucketGroupId, + Name = "Bucket Active Past", + ValidFrom = new DateTime(2009, 1, 1) + }; + var testBucket3 = new Bucket() + { + BucketGroupId = testBucketGroup.BucketGroupId, + Name = "Bucket Active Future", + ValidFrom = new DateTime(2010, 2, 1) + }; + + dbContext.CreateBuckets(new[] + { + testBucket1, testBucket2, testBucket3 + }); + dbContext.CreateBucketVersions(new[] + { + new BucketVersion() { BucketId = testBucket1.BucketId, Version = 1, BucketType = 1, ValidFrom = testBucket1.ValidFrom }, + new BucketVersion() { BucketId = testBucket2.BucketId, Version = 1, BucketType = 1, ValidFrom = testBucket2.ValidFrom }, + new BucketVersion() { BucketId = testBucket3.BucketId, Version = 1, BucketType = 1, ValidFrom = testBucket3.ValidFrom }, + }); + + + var monthSelectorViewModel = new YearMonthSelectorViewModel() + { + SelectedYear = 2010, + SelectedMonth = 1 + }; + var viewModel = new BucketViewModel(_dbOptions, monthSelectorViewModel); + await viewModel.LoadDataAsync(); + + var bucketGroup = viewModel.BucketGroups.FirstOrDefault(i => i.BucketGroup.BucketGroupId == testBucketGroup.BucketGroupId); + Assert.NotNull(bucketGroup); + + Assert.Equal(2, bucketGroup.Buckets.Count); + Assert.Contains(bucketGroup.Buckets, i => i.Bucket.BucketId == testBucket1.BucketId); + Assert.Contains(bucketGroup.Buckets, i => i.Bucket.BucketId == testBucket2.BucketId); + Assert.DoesNotContain(bucketGroup.Buckets, i => i.Bucket.BucketId == testBucket3.BucketId); + } + } + finally + { + DbConnector.CleanupDatabase(nameof(BucketViewModelTest)); + } + } + + [Fact] + public async Task LoadDataAsync_CheckCalculatedValues() + { + try + { + using (var dbContext = new DatabaseContext(_dbOptions)) + { + var testAccount = new Account() { IsActive = 1, Name = "Account" }; + var testBucketGroup = new BucketGroup() { Name = "Bucket Group", Position = 1 }; + + dbContext.CreateAccount(testAccount); + dbContext.CreateBucketGroup(testBucketGroup); + + var testBucket1 = new Bucket() + { + BucketGroupId = testBucketGroup.BucketGroupId, + Name = "Bucket 1", + ValidFrom = new DateTime(2010, 1, 1) + }; + var testBucket2 = new Bucket() + { + BucketGroupId = testBucketGroup.BucketGroupId, + Name = "Bucket 2", + ValidFrom = new DateTime(2010, 1, 1) + }; + + dbContext.CreateBuckets(new[] + { + testBucket1, testBucket2 + }); + dbContext.CreateBucketVersions(new[] + { + new BucketVersion() { BucketId = testBucket1.BucketId, Version = 1, BucketType = 1, ValidFrom = testBucket1.ValidFrom }, + new BucketVersion() { BucketId = testBucket2.BucketId, Version = 1, BucketType = 1, ValidFrom = testBucket2.ValidFrom }, + }); + + var testTransactions = new[] + { + new BankTransaction() { AccountId = testAccount.AccountId, TransactionDate = new DateTime(2010,1,1), Amount = 1 }, + new BankTransaction() { AccountId = testAccount.AccountId, TransactionDate = new DateTime(2010,1,1), Amount = -10 }, + new BankTransaction() { AccountId = testAccount.AccountId, TransactionDate = new DateTime(2010,1,1), Amount = 100 }, + new BankTransaction() { AccountId = testAccount.AccountId, TransactionDate = new DateTime(2010,1,1), Amount = -1000 }, + new BankTransaction() { AccountId = testAccount.AccountId, TransactionDate = new DateTime(2010,1,1), Amount = 10000 }, + new BankTransaction() { AccountId = testAccount.AccountId, TransactionDate = new DateTime(2009,1,1), Amount = 100000 }, + new BankTransaction() { AccountId = testAccount.AccountId, TransactionDate = new DateTime(2010,2,1), Amount = 1000000 }, + }; + dbContext.CreateBankTransactions(testTransactions); + + dbContext.CreateBudgetedTransactions(new[] + { + new BudgetedTransaction() { TransactionId = testTransactions[0].TransactionId, BucketId = testBucket1.BucketId, Amount = 1 }, + new BudgetedTransaction() { TransactionId = testTransactions[1].TransactionId, BucketId = testBucket1.BucketId, Amount = -5 }, + new BudgetedTransaction() { TransactionId = testTransactions[1].TransactionId, BucketId = testBucket2.BucketId, Amount = -5 }, + new BudgetedTransaction() { TransactionId = testTransactions[2].TransactionId, BucketId = testBucket1.BucketId, Amount = 100 }, + new BudgetedTransaction() { TransactionId = testTransactions[3].TransactionId, BucketId = testBucket2.BucketId, Amount = -1000 }, + new BudgetedTransaction() { TransactionId = testTransactions[4].TransactionId, BucketId = testBucket2.BucketId, Amount = 10000 }, + new BudgetedTransaction() { TransactionId = testTransactions[5].TransactionId, BucketId = testBucket2.BucketId, Amount = 100000 }, + new BudgetedTransaction() { TransactionId = testTransactions[6].TransactionId, BucketId = testBucket2.BucketId, Amount = 1000000 }, + }); + + var monthSelectorViewModel = new YearMonthSelectorViewModel() + { + SelectedYear = 2010, + SelectedMonth = 1 + }; + var viewModel = new BucketViewModel(_dbOptions, monthSelectorViewModel); + await viewModel.LoadDataAsync(); + + var bucketGroup = viewModel.BucketGroups.FirstOrDefault(i => i.BucketGroup.BucketGroupId == testBucketGroup.BucketGroupId); + Assert.NotNull(bucketGroup); + + // This test includes: + // - Bucket Split + var testObject = bucketGroup.Buckets.FirstOrDefault(i => i.Bucket.BucketId == testBucket1.BucketId); + Assert.NotNull(testObject); + Assert.Equal(-5, testObject.Activity); + Assert.Equal(96, testObject.Balance); + Assert.Equal(101, testObject.In); + + // This test includes: + // - Bucket Split + // - Include Transactions in previous months for Balance + // - Exclude Transactions in the future + testObject = bucketGroup.Buckets.FirstOrDefault(i => i.Bucket.BucketId == testBucket2.BucketId); + Assert.NotNull(testObject); + Assert.Equal(-1005, testObject.Activity); + Assert.Equal(108995, testObject.Balance); + Assert.Equal(10000, testObject.In); + } + } + finally + { + DbConnector.CleanupDatabase(nameof(BucketViewModelTest)); + } + } + + public static IEnumerable TestData_CheckWantAndDetailCalculation_MonthlyExpenses + { + get + { + return new[] + { + new object[] + { + new Bucket { Name = "Bucket with pending Want", ValidFrom = new DateTime(2010,1,1) }, + new BucketVersion { Version = 1, BucketType = 2, BucketTypeYParam = 10 }, + new List(), + new List(), + 10, 0, 0 + }, + new object[] + { + new Bucket { Name = "Bucket with fulfilled Want", ValidFrom = new DateTime(2010,1,1) }, + new BucketVersion { Version = 1, BucketType = 2, BucketTypeYParam = 10 }, + new List(), + new List() + { + new BucketMovement { MovementDate = new DateTime(2010, 1, 1), Amount = 10 } + }, + 0, 10 ,0 + }, + new object[] + { + new Bucket { Name = "Bucket pending Want including expense", ValidFrom = new DateTime(2010,1,1) }, + new BucketVersion { Version = 1, BucketType = 2, BucketTypeYParam = 10 }, + new List + { + new BankTransaction { TransactionDate = new DateTime(2010,1,1), Amount = -10 } + }, + new List(), + 10, 0, -10 + }, + new object[] + { + new Bucket { Name = "Bucket fulfilled Want including expense", ValidFrom = new DateTime(2010,1,1) }, + new BucketVersion { Version = 1, BucketType = 2, BucketTypeYParam = 10 }, + new List + { + new BankTransaction { TransactionDate = new DateTime(2010,1,1), Amount = -10 } + }, + new List + { + new BucketMovement { MovementDate = new DateTime(2010, 1, 1), Amount = 10 } + }, + 0, 10, -10 + }, + new object[] + { + new Bucket { Name = "Bucket with partial fulfilled Want", ValidFrom = new DateTime(2010,1,1) }, + new BucketVersion { Version = 1, BucketType = 2, BucketTypeYParam = 10 }, + new List(), + new List + { + new BucketMovement { MovementDate = new DateTime(2010, 1, 1), Amount = 5 } + }, + 5, 5, 0 + }, + new object[] + { + new Bucket { Name = "Bucket with over fulfilled Want", ValidFrom = new DateTime(2010,1,1) }, + new BucketVersion { Version = 1, BucketType = 2, BucketTypeYParam = 10 }, + new List(), + new List + { + new BucketMovement { MovementDate = new DateTime(2010, 1, 1), Amount = 15 } + }, + 0, 15 ,0 + } + }; + } + } + + [Theory] + [MemberData(nameof(TestData_CheckWantAndDetailCalculation_MonthlyExpenses))] + public async Task LoadDataAsync_CheckWantAndDetailCalculation_MonthlyExpenses( + Bucket testBucket, + BucketVersion testBucketVersion, + List testTransactions, + List testBucketMovements, + decimal expectedWant, + decimal expectedIn, + decimal expectedActivity + ) + { + try + { + var testObject = await ExecuteBucketCreationAndTransactionMovementsAsync( + testBucket, testBucketVersion, testTransactions, testBucketMovements, new DateTime(2010,1,1)); + + Assert.Equal(expectedWant, testObject.Want); + Assert.Equal(expectedIn, testObject.In); + Assert.Equal(expectedActivity, testObject.Activity); + + } + finally + { + DbConnector.CleanupDatabase(nameof(BucketViewModelTest)); + } + } + + public static IEnumerable TestData_CheckWantAndDetailCalculation_ExpenseEveryXMonths + { + get + { + return new[] + { + new object[] + { + new Bucket { Name = "120 every 12 months, with Want", ValidFrom = new DateTime(2010,1,1) }, + new BucketVersion { Version = 1, BucketType = 3, BucketTypeXParam = 12, BucketTypeYParam = 120, BucketTypeZParam = new DateTime(2010,12,1) }, + new List(), + new List(), + 10, 0, 0, 0, "120 until 2010-12", 0 + }, + new object[] + { + new Bucket { Name = "120 every 12 months, without Want", ValidFrom = new DateTime(2010,1,1) }, + new BucketVersion { Version = 1, BucketType = 3, BucketTypeXParam = 12, BucketTypeYParam = 120, BucketTypeZParam = new DateTime(2010,12,1) }, + new List(), + new List + { + new BucketMovement { MovementDate = new DateTime(2010,1,1), Amount = 10 } + }, + 0, 10, 0, 10, "120 until 2010-12", 8 + }, + new object[] + { + new Bucket { Name = "120 every 12 months, last 6 months, with Want", ValidFrom = new DateTime(2009,7,1) }, + new BucketVersion { Version = 1, BucketType = 3, BucketTypeXParam = 12, BucketTypeYParam = 120, BucketTypeZParam = new DateTime(2010,6,1) }, + new List(), + new List + { + new BucketMovement { MovementDate = new DateTime(2009,7,1), Amount = 10 }, + new BucketMovement { MovementDate = new DateTime(2009,8,1), Amount = 10 }, + new BucketMovement { MovementDate = new DateTime(2009,9,1), Amount = 10 }, + new BucketMovement { MovementDate = new DateTime(2009,10,1), Amount = 10 }, + new BucketMovement { MovementDate = new DateTime(2009,11,1), Amount = 10 }, + new BucketMovement { MovementDate = new DateTime(2009,12,1), Amount = 10 } + }, + 10, 0, 0, 60, "120 until 2010-06", 50 + }, + new object[] + { + new Bucket { Name = "120 every 12 months, last 6 months, without Want", ValidFrom = new DateTime(2009,7,1) }, + new BucketVersion { Version = 1, BucketType = 3, BucketTypeXParam = 12, BucketTypeYParam = 120, BucketTypeZParam = new DateTime(2010,6,1) }, + new List(), + new List + { + new BucketMovement { MovementDate = new DateTime(2009,7,1), Amount = 10 }, + new BucketMovement { MovementDate = new DateTime(2009,8,1), Amount = 10 }, + new BucketMovement { MovementDate = new DateTime(2009,9,1), Amount = 10 }, + new BucketMovement { MovementDate = new DateTime(2009,10,1), Amount = 10 }, + new BucketMovement { MovementDate = new DateTime(2009,11,1), Amount = 10 }, + new BucketMovement { MovementDate = new DateTime(2009,12,1), Amount = 10 }, + new BucketMovement { MovementDate = new DateTime(2010,1,1), Amount = 10 } + }, + 0, 10, 0, 70, "120 until 2010-06", 58 + }, + new object[] + { + new Bucket { Name = "120 every 12 months, last 6 months, fulfilled target", ValidFrom = new DateTime(2009,7,1) }, + new BucketVersion { Version = 1, BucketType = 3, BucketTypeXParam = 12, BucketTypeYParam = 120, BucketTypeZParam = new DateTime(2010,6,1) }, + new List(), + new List + { + new BucketMovement { MovementDate = new DateTime(2009,7,1), Amount = 20 }, + new BucketMovement { MovementDate = new DateTime(2009,8,1), Amount = 20 }, + new BucketMovement { MovementDate = new DateTime(2009,9,1), Amount = 20 }, + new BucketMovement { MovementDate = new DateTime(2009,10,1), Amount = 20 }, + new BucketMovement { MovementDate = new DateTime(2009,11,1), Amount = 20 }, + new BucketMovement { MovementDate = new DateTime(2009,12,1), Amount = 20 } + }, + 0, 0, 0, 120, "120 until 2010-06", 100 + }, + new object[] + { + new Bucket { Name = "120 every 12 months, last 6 months, over-fulfilled target", ValidFrom = new DateTime(2009,7,1) }, + new BucketVersion { Version = 1, BucketType = 3, BucketTypeXParam = 12, BucketTypeYParam = 120, BucketTypeZParam = new DateTime(2010,6,1) }, + new List(), + new List + { + new BucketMovement { MovementDate = new DateTime(2009,7,1), Amount = 20 }, + new BucketMovement { MovementDate = new DateTime(2009,8,1), Amount = 20 }, + new BucketMovement { MovementDate = new DateTime(2009,9,1), Amount = 20 }, + new BucketMovement { MovementDate = new DateTime(2009,10,1), Amount = 20 }, + new BucketMovement { MovementDate = new DateTime(2009,11,1), Amount = 20 }, + new BucketMovement { MovementDate = new DateTime(2009,12,1), Amount = 30 } + }, + 0, 0, 0, 130, "120 until 2010-06", 100 + }, + new object[] + { + new Bucket { Name = "120 every 12 months, last 6 months, no input", ValidFrom = new DateTime(2009,7,1) }, + new BucketVersion { Version = 1, BucketType = 3, BucketTypeXParam = 12, BucketTypeYParam = 120, BucketTypeZParam = new DateTime(2010,6,1) }, + new List(), + new List(), + 20, 0, 0, 0, "120 until 2010-06", 0 + }, + new object[] + { + new Bucket { Name = "120 every 12 months, last 6 months, input not in sync", ValidFrom = new DateTime(2009,7,1) }, + new BucketVersion { Version = 1, BucketType = 3, BucketTypeXParam = 12, BucketTypeYParam = 120, BucketTypeZParam = new DateTime(2010,6,1) }, + new List(), + new List + { + new BucketMovement { MovementDate = new DateTime(2009,7,1), Amount = 10 }, + new BucketMovement { MovementDate = new DateTime(2009,8,1), Amount = 10 }, + new BucketMovement { MovementDate = new DateTime(2009,11,1), Amount = 10 } + }, + 15, 0, 0, 30, "120 until 2010-06", 25 + }, + new object[] + { + new Bucket { Name = "100 every 3 months, with Want", ValidFrom = new DateTime(2010,1,1) }, + new BucketVersion { Version = 1, BucketType = 3, BucketTypeXParam = 3, BucketTypeYParam = 100, BucketTypeZParam = new DateTime(2010,3,1) }, + new List(), + new List(), + 33.33m, 0, 0, 0, "100 until 2010-03", 0 + }, + new object[] + { + new Bucket { Name = "100 every 3 months, last month, with Want", ValidFrom = new DateTime(2009,11,1) }, + new BucketVersion { Version = 1, BucketType = 3, BucketTypeXParam = 3, BucketTypeYParam = 100, BucketTypeZParam = new DateTime(2010,1,1) }, + new List(), + new List + { + new BucketMovement { MovementDate = new DateTime(2009,11,1), Amount = 33.33m }, + new BucketMovement { MovementDate = new DateTime(2009,12,1), Amount = 33.33m } + }, + 33.34m, 0, 0, 66.66m, "100 until 2010-01", 67 + }, + new object[] + { + new Bucket { Name = "100 every 3 months, last month, input not in sync", ValidFrom = new DateTime(2009,11,1) }, + new BucketVersion { Version = 1, BucketType = 3, BucketTypeXParam = 3, BucketTypeYParam = 100, BucketTypeZParam = new DateTime(2010,1,1) }, + new List(), + new List + { + new BucketMovement { MovementDate = new DateTime(2009,11,1), Amount = 12.34m }, + new BucketMovement { MovementDate = new DateTime(2009,12,1), Amount = 56.78m } + }, + 30.88m, 0, 0, 69.12m, "100 until 2010-01", 69 + }, + new object[] + { + new Bucket { Name = "120 every 12 months, last 6 months, with expenses", ValidFrom = new DateTime(2009,7,1) }, + new BucketVersion { Version = 1, BucketType = 3, BucketTypeXParam = 12, BucketTypeYParam = 120, BucketTypeZParam = new DateTime(2010,6,1) }, + new List + { + new BankTransaction { TransactionDate = new DateTime(2009,9,2), Amount = -30 }, + new BankTransaction { TransactionDate = new DateTime(2010,1,2), Amount = -10 } + }, + new List + { + new BucketMovement { MovementDate = new DateTime(2009,7,1), Amount = 10 }, + new BucketMovement { MovementDate = new DateTime(2009,8,1), Amount = 10 }, + new BucketMovement { MovementDate = new DateTime(2009,9,1), Amount = 10 }, + new BucketMovement { MovementDate = new DateTime(2009,10,1), Amount = 10 }, + new BucketMovement { MovementDate = new DateTime(2009,11,1), Amount = 10 }, + new BucketMovement { MovementDate = new DateTime(2009,12,1), Amount = 10 } + }, + 16.67m, 0, -10, 20, "120 until 2010-06", 17 + }, + new object[] + { + new Bucket { Name = "120 every 12 months, 2nd year, last 6 months, with Want", ValidFrom = new DateTime(2008,7,1) }, + new BucketVersion { Version = 1, BucketType = 3, BucketTypeXParam = 12, BucketTypeYParam = 120, BucketTypeZParam = new DateTime(2009,6,1) }, + new List + { + new BankTransaction { TransactionDate = new DateTime(2009,6,1), Amount = -120 } + }, + new List + { + new BucketMovement { MovementDate = new DateTime(2008,7,1), Amount = 10 }, + new BucketMovement { MovementDate = new DateTime(2008,8,1), Amount = 10 }, + new BucketMovement { MovementDate = new DateTime(2008,9,1), Amount = 10 }, + new BucketMovement { MovementDate = new DateTime(2008,10,1), Amount = 10 }, + new BucketMovement { MovementDate = new DateTime(2008,11,1), Amount = 10 }, + new BucketMovement { MovementDate = new DateTime(2008,12,1), Amount = 10 }, + new BucketMovement { MovementDate = new DateTime(2009,1,1), Amount = 10 }, + new BucketMovement { MovementDate = new DateTime(2009,2,1), Amount = 10 }, + new BucketMovement { MovementDate = new DateTime(2009,3,1), Amount = 10 }, + new BucketMovement { MovementDate = new DateTime(2009,4,1), Amount = 10 }, + new BucketMovement { MovementDate = new DateTime(2009,5,1), Amount = 10 }, + new BucketMovement { MovementDate = new DateTime(2009,6,1), Amount = 10 }, + new BucketMovement { MovementDate = new DateTime(2009,7,1), Amount = 10 }, + new BucketMovement { MovementDate = new DateTime(2009,8,1), Amount = 10 }, + new BucketMovement { MovementDate = new DateTime(2009,9,1), Amount = 10 }, + new BucketMovement { MovementDate = new DateTime(2009,10,1), Amount = 10 }, + new BucketMovement { MovementDate = new DateTime(2009,11,1), Amount = 10 }, + new BucketMovement { MovementDate = new DateTime(2009,12,1), Amount = 10 } + }, + 10, 0, 0, 60, "120 until 2010-06", 50 + } + }; + } + } + + public static IEnumerable TestData_CheckWantAndDetailCalculation_SaveXUntilY + { + get + { + return new[] + { + new object[] + { + new Bucket { Name = "120 until 2010-12, no input", ValidFrom = new DateTime(2010,1,1) }, + new BucketVersion { Version = 1, BucketType = 4, BucketTypeYParam = 120, BucketTypeZParam = new DateTime(2010,12,1) }, + new List(), + new List(), + 10, 0, 0, 0, "120 until 2010-12", 0 + }, + new object[] + { + new Bucket { Name = "120 until 2010-12, input in current Month", ValidFrom = new DateTime(2010,1,1) }, + new BucketVersion { Version = 1, BucketType = 4, BucketTypeYParam = 120, BucketTypeZParam = new DateTime(2010,12,1) }, + new List(), + new List + { + new BucketMovement { MovementDate = new DateTime(2010,1,1), Amount = 10 } + }, + 0, 10, 0, 10, "120 until 2010-12", 8 + }, + new object[] + { + new Bucket { Name = "120 until 2010-06, input in sync", ValidFrom = new DateTime(2009,7,1) }, + new BucketVersion { Version = 1, BucketType = 4, BucketTypeYParam = 120, BucketTypeZParam = new DateTime(2010,6,1) }, + new List(), + new List + { + new BucketMovement { MovementDate = new DateTime(2009,7,1), Amount = 10 }, + new BucketMovement { MovementDate = new DateTime(2009,8,1), Amount = 10 }, + new BucketMovement { MovementDate = new DateTime(2009,9,1), Amount = 10 }, + new BucketMovement { MovementDate = new DateTime(2009,10,1), Amount = 10 }, + new BucketMovement { MovementDate = new DateTime(2009,11,1), Amount = 10 }, + new BucketMovement { MovementDate = new DateTime(2009,12,1), Amount = 10 } + }, + 10, 0, 0, 60, "120 until 2010-06", 50 + }, + new object[] + { + new Bucket { Name = "120 until 2010-06, fulfilled target", ValidFrom = new DateTime(2009,7,1) }, + new BucketVersion { Version = 1, BucketType = 4, BucketTypeYParam = 120, BucketTypeZParam = new DateTime(2010,6,1) }, + new List(), + new List + { + new BucketMovement { MovementDate = new DateTime(2009,7,1), Amount = 20 }, + new BucketMovement { MovementDate = new DateTime(2009,8,1), Amount = 20 }, + new BucketMovement { MovementDate = new DateTime(2009,9,1), Amount = 20 }, + new BucketMovement { MovementDate = new DateTime(2009,10,1), Amount = 20 }, + new BucketMovement { MovementDate = new DateTime(2009,11,1), Amount = 20 }, + new BucketMovement { MovementDate = new DateTime(2009,12,1), Amount = 20 } + }, + 0, 0, 0, 120, "120 until 2010-06", 100 + }, + new object[] + { + new Bucket { Name = "120 until 2010-06, over-fulfilled target", ValidFrom = new DateTime(2009,7,1) }, + new BucketVersion { Version = 1, BucketType = 4, BucketTypeYParam = 120, BucketTypeZParam = new DateTime(2010,6,1) }, + new List(), + new List + { + new BucketMovement { MovementDate = new DateTime(2009,7,1), Amount = 20 }, + new BucketMovement { MovementDate = new DateTime(2009,8,1), Amount = 20 }, + new BucketMovement { MovementDate = new DateTime(2009,9,1), Amount = 20 }, + new BucketMovement { MovementDate = new DateTime(2009,10,1), Amount = 20 }, + new BucketMovement { MovementDate = new DateTime(2009,11,1), Amount = 20 }, + new BucketMovement { MovementDate = new DateTime(2009,12,1), Amount = 30 } + }, + 0, 0, 0, 130, "120 until 2010-06", 100 + }, + new object[] + { + new Bucket { Name = "120 until 2010-06, input not in sync", ValidFrom = new DateTime(2009,7,1) }, + new BucketVersion { Version = 1, BucketType = 4, BucketTypeYParam = 120, BucketTypeZParam = new DateTime(2010,6,1) }, + new List(), + new List + { + new BucketMovement { MovementDate = new DateTime(2009,7,1), Amount = 10 }, + new BucketMovement { MovementDate = new DateTime(2009,9,1), Amount = 10 }, + new BucketMovement { MovementDate = new DateTime(2009,10,1), Amount = 10 } + }, + 15, 0, 0, 30, "120 until 2010-06", 25 + }, + new object[] + { + new Bucket { Name = "120 until 2009-12, target not reached", ValidFrom = new DateTime(2009,7,1) }, + new BucketVersion { Version = 1, BucketType = 4, BucketTypeYParam = 120, BucketTypeZParam = new DateTime(2009,12,1) }, + new List(), + new List + { + new BucketMovement { MovementDate = new DateTime(2009,7,1), Amount = 10 }, + new BucketMovement { MovementDate = new DateTime(2009,9,1), Amount = 10 }, + new BucketMovement { MovementDate = new DateTime(2009,10,1), Amount = 10 } + }, + 90, 0, 0, 30, "120 until 2009-12", 25 + }, + new object[] + { + new Bucket { Name = "30 until 2009-12, target reached, with expense in target month", ValidFrom = new DateTime(2009,7,1) }, + new BucketVersion { Version = 1, BucketType = 4, BucketTypeYParam = 30, BucketTypeZParam = new DateTime(2009,12,1) }, + new List + { + new BankTransaction { TransactionDate = new DateTime(2009,12,1), Amount = -30 } + }, + new List + { + new BucketMovement { MovementDate = new DateTime(2009,7,1), Amount = 10 }, + new BucketMovement { MovementDate = new DateTime(2009,9,1), Amount = 10 }, + new BucketMovement { MovementDate = new DateTime(2009,10,1), Amount = 10 } + }, + 0, 0, 0, 0, "30 until 2009-12", 100 + } + }; + } + } + + [Theory] + [MemberData(nameof(TestData_CheckWantAndDetailCalculation_ExpenseEveryXMonths))] + [MemberData(nameof(TestData_CheckWantAndDetailCalculation_SaveXUntilY))] + public async Task LoadDataAsync_CheckWantAndDetailCalculation_ExpenseEveryXMonths_SaveXUntilY( + Bucket testBucket, + BucketVersion testBucketVersion, + List testTransactions, + List testBucketMovements, + decimal expectedWant, + decimal expectedIn, + decimal expectedActivity, + decimal expectedBalance, + string expectedDetails, + int expectedProgress + ) + { + try + { + var testObject = await ExecuteBucketCreationAndTransactionMovementsAsync( + testBucket, testBucketVersion, testTransactions, testBucketMovements, new DateTime(2010,1,1)); + + Assert.Equal(expectedWant, testObject.Want); + Assert.Equal(expectedIn, testObject.In); + Assert.Equal(expectedActivity, testObject.Activity); + Assert.Equal(expectedBalance, testObject.Balance); + Assert.Equal(expectedDetails, testObject.Details); + Assert.Equal(expectedProgress, testObject.Progress); + } + finally + { + DbConnector.CleanupDatabase(nameof(BucketViewModelTest)); + } + } + + private async Task ExecuteBucketCreationAndTransactionMovementsAsync( + Bucket testBucket, + BucketVersion testBucketVersion, + IEnumerable testTransactions, + IEnumerable testBucketMovements, + DateTime testMonth) + { + using (var dbContext = new DatabaseContext(_dbOptions)) + { + var testAccount = new Account() { IsActive = 1, Name = "Account" }; + var testBucketGroup = new BucketGroup() { Name = "Bucket Group", Position = 1 }; + + dbContext.CreateAccount(testAccount); + dbContext.CreateBucketGroup(testBucketGroup); + + testBucket.BucketGroupId = testBucketGroup.BucketGroupId; + dbContext.CreateBucket(testBucket); + + testBucketVersion.BucketId = testBucket.BucketId; + testBucketVersion.ValidFrom = testBucket.ValidFrom; + dbContext.CreateBucketVersion(testBucketVersion); + + foreach (var testTransaction in testTransactions) + { + testTransaction.AccountId = testAccount.AccountId; + dbContext.CreateBankTransaction(testTransaction); + dbContext.CreateBudgetedTransaction(new BudgetedTransaction() + { + TransactionId = testTransaction.TransactionId, + BucketId = testBucket.BucketId, + Amount = testTransaction.Amount + }); + } + + foreach (var testBucketMovement in testBucketMovements) + { + testBucketMovement.BucketId = testBucket.BucketId; + dbContext.CreateBucketMovement(testBucketMovement); + } + + var monthSelectorViewModel = new YearMonthSelectorViewModel() + { + SelectedYear = testMonth.Year, + SelectedMonth = testMonth.Month + }; + var viewModel = new BucketViewModel(_dbOptions, monthSelectorViewModel); + await viewModel.LoadDataAsync(); + + var bucketGroup = viewModel.BucketGroups.FirstOrDefault(i => i.BucketGroup.BucketGroupId == testBucketGroup.BucketGroupId); + Assert.NotNull(bucketGroup); + + var testObject = bucketGroup.Buckets.FirstOrDefault(i => i.Bucket.BucketId == testBucket.BucketId); + Assert.NotNull(testObject); + + return testObject; + } + } + + public async Task DistributeBudget_CheckDistributedMoney( + IEnumerable> testBuckets + ) + { + using (var dbContext = new DatabaseContext(_dbOptions)) + { + var testAccount = new Account() { IsActive = 1, Name = "Account" }; + var testBucketGroup = new BucketGroup() { Name = "Bucket Group", Position = 1 }; + + dbContext.CreateAccount(testAccount); + dbContext.CreateBucketGroup(testBucketGroup); + + foreach (var (bucket, bucketVersion) in testBuckets) + { + bucket.BucketGroupId = testBucketGroup.BucketGroupId; + dbContext.CreateBucket(bucket); + bucketVersion.BucketId = bucket.BucketId; + dbContext.CreateBucketVersion(bucketVersion); + } + } + } + } +} diff --git a/OpenBudgeteer.Core.Test/ViewModelTest/YearMonthSelectorViewModelTest.cs b/OpenBudgeteer.Core.Test/ViewModelTest/YearMonthSelectorViewModelTest.cs new file mode 100644 index 0000000..fa19376 --- /dev/null +++ b/OpenBudgeteer.Core.Test/ViewModelTest/YearMonthSelectorViewModelTest.cs @@ -0,0 +1,123 @@ +using System; +using System.Globalization; +using System.Linq; +using OpenBudgeteer.Core.Common; +using OpenBudgeteer.Core.ViewModels; +using Xunit; + +namespace OpenBudgeteer.Core.Test.ViewModelTest +{ + public class YearMonthSelectorViewModelTest + { + [Fact] + public void Constructor_CheckDefaults() + { + var viewModel = new YearMonthSelectorViewModel(); + + Assert.Equal(DateTime.Now.Year, viewModel.SelectedYear); + Assert.Equal(DateTime.Now.Month, viewModel.SelectedMonth); + Assert.Equal(new DateTime(DateTime.Now.Year, DateTime.Now.Month, 1), viewModel.CurrentMonth); + + // Test Months + Assert.Equal(12, viewModel.Months.Count); + for (int i = 1; i < 13; i++) + { + Assert.Equal(i, viewModel.Months.ElementAt(i-1)); + } + + var cultureInfo = new CultureInfo("de-DE"); + CultureInfo.DefaultThreadCurrentCulture = cultureInfo; + CultureInfo.DefaultThreadCurrentUICulture = cultureInfo; + var converter = new MonthOutputConverter(); + Assert.Equal("Jan", converter.ConvertMonth(1)); + Assert.Equal("Feb", converter.ConvertMonth(2)); + Assert.Equal("Mrz", converter.ConvertMonth(3)); + Assert.Equal("Apr", converter.ConvertMonth(4)); + Assert.Equal("Mai", converter.ConvertMonth(5)); + Assert.Equal("Jun", converter.ConvertMonth(6)); + Assert.Equal("Jul", converter.ConvertMonth(7)); + Assert.Equal("Aug", converter.ConvertMonth(8)); + Assert.Equal("Sep", converter.ConvertMonth(9)); + Assert.Equal("Okt", converter.ConvertMonth(10)); + Assert.Equal("Nov", converter.ConvertMonth(11)); + Assert.Equal("Dez", converter.ConvertMonth(12)); + } + + [Fact] + public void PreviousMonth_CheckMonth() + { + var viewModel = new YearMonthSelectorViewModel() + { + SelectedYear = 2010, + SelectedMonth = 2 + }; + + viewModel.PreviousMonth(); + + Assert.Equal(2010, viewModel.SelectedYear); + Assert.Equal(1, viewModel.SelectedMonth); + + viewModel.PreviousMonth(); + + Assert.Equal(2009, viewModel.SelectedYear); + Assert.Equal(12, viewModel.SelectedMonth); + } + + [Fact] + public void NextMonth_CheckMonth() + { + var viewModel = new YearMonthSelectorViewModel() + { + SelectedYear = 2009, + SelectedMonth = 11 + }; + + viewModel.NextMonth(); + + Assert.Equal(2009, viewModel.SelectedYear); + Assert.Equal(12, viewModel.SelectedMonth); + + viewModel.NextMonth(); + + Assert.Equal(2010, viewModel.SelectedYear); + Assert.Equal(1, viewModel.SelectedMonth); + } + + [Fact] + public void SelectedYearMonthChanged_CheckEventHasBeenInvoked() + { + var viewModel = new YearMonthSelectorViewModel() + { + SelectedYear = 2010, + SelectedMonth = 1 + }; + var eventHasBeenInvoked = false; + viewModel.SelectedYearMonthChanged += (sender, args) => eventHasBeenInvoked = true; + + viewModel.SelectedYear = 2010; + Assert.False(eventHasBeenInvoked); + + eventHasBeenInvoked = false; + viewModel.SelectedMonth = 1; + Assert.False(eventHasBeenInvoked); + + eventHasBeenInvoked = false; + viewModel.SelectedYear = 2009; + Assert.True(eventHasBeenInvoked); + + eventHasBeenInvoked = false; + viewModel.SelectedMonth = 2; + Assert.True(eventHasBeenInvoked); + + eventHasBeenInvoked = false; + viewModel.NextMonth(); + Assert.True(eventHasBeenInvoked); + + eventHasBeenInvoked = false; + viewModel.PreviousMonth(); + Assert.True(eventHasBeenInvoked); + + + } + } +} diff --git a/OpenBudgeteer.Core/Common/DatabaseContext.cs b/OpenBudgeteer.Core/Common/Database/DatabaseContext.cs similarity index 99% rename from OpenBudgeteer.Core/Common/DatabaseContext.cs rename to OpenBudgeteer.Core/Common/Database/DatabaseContext.cs index d1e3f8e..1b45fe2 100644 --- a/OpenBudgeteer.Core/Common/DatabaseContext.cs +++ b/OpenBudgeteer.Core/Common/Database/DatabaseContext.cs @@ -6,7 +6,7 @@ using Microsoft.EntityFrameworkCore; using OpenBudgeteer.Core.Models; -namespace OpenBudgeteer.Core.Common +namespace OpenBudgeteer.Core.Common.Database { public class DatabaseContext : DbContext { @@ -20,7 +20,7 @@ public class DatabaseContext : DbContext public DbSet ImportProfile { get; set; } public DbSet BucketRuleSet { get; set; } public DbSet MappingRule { get; set; } - + public DatabaseContext(DbContextOptions options) : base(options) { } #region Create diff --git a/OpenBudgeteer.Core/Common/Database/MySqlDatabaseContext.cs b/OpenBudgeteer.Core/Common/Database/MySqlDatabaseContext.cs new file mode 100644 index 0000000..d8fe386 --- /dev/null +++ b/OpenBudgeteer.Core/Common/Database/MySqlDatabaseContext.cs @@ -0,0 +1,15 @@ +using System; +using System.Collections.Generic; +using System.Text; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Design; + +namespace OpenBudgeteer.Core.Common.Database +{ + public class MySqlDatabaseContext : DatabaseContext + { + public MySqlDatabaseContext(DbContextOptions options) : base(options) + { + } + } +} diff --git a/OpenBudgeteer.Core/Common/Database/MySqlDatabaseContextFactory.cs b/OpenBudgeteer.Core/Common/Database/MySqlDatabaseContextFactory.cs new file mode 100644 index 0000000..06cc616 --- /dev/null +++ b/OpenBudgeteer.Core/Common/Database/MySqlDatabaseContextFactory.cs @@ -0,0 +1,39 @@ +using System; +using System.Collections.Generic; +using System.Text; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Design; +using Microsoft.Extensions.Configuration; + +namespace OpenBudgeteer.Core.Common.Database +{ + public class MySqlDatabaseContextFactory : IDesignTimeDbContextFactory + { + public MySqlDatabaseContext CreateDbContext(string[] args) + { + var optionsBuilder = new DbContextOptionsBuilder() + .UseMySql("Server=192.168.178.30;" + + "Port=3306;" + + "Database=openbudgeteer-dev" + + "User=openbudgeteer-dev" + + "Password=openbudgeteer-dev"); + + return new MySqlDatabaseContext(optionsBuilder.Options); + } + + public MySqlDatabaseContext CreateDbContext(IConfiguration configuration) + { + var configurationSection = configuration.GetSection("Connection"); + var connectionString = $"Server={configurationSection?["Server"]};" + + $"Port={configurationSection?["Port"]};" + + $"Database={configurationSection?["Database"]};" + + $"User={configurationSection?["User"]};" + + $"Password={configurationSection?["Password"]}"; + var optionsBuilder = new DbContextOptionsBuilder() + .UseMySql( + connectionString, + b => b.MigrationsAssembly("OpenBudgeteer.Core")); + return new MySqlDatabaseContext(optionsBuilder.Options); + } + } +} diff --git a/OpenBudgeteer.Core/Common/Database/SqliteDatabaseContext.cs b/OpenBudgeteer.Core/Common/Database/SqliteDatabaseContext.cs new file mode 100644 index 0000000..29df075 --- /dev/null +++ b/OpenBudgeteer.Core/Common/Database/SqliteDatabaseContext.cs @@ -0,0 +1,15 @@ +using System; +using System.Collections.Generic; +using System.Text; +using Microsoft.EntityFrameworkCore; + +namespace OpenBudgeteer.Core.Common.Database +{ + public class SqliteDatabaseContext : DatabaseContext + { + public SqliteDatabaseContext(DbContextOptions options) : base(options) + { + + } + } +} diff --git a/OpenBudgeteer.Core/Common/Database/SqliteDatabaseContextFactory.cs b/OpenBudgeteer.Core/Common/Database/SqliteDatabaseContextFactory.cs new file mode 100644 index 0000000..a4da3dc --- /dev/null +++ b/OpenBudgeteer.Core/Common/Database/SqliteDatabaseContextFactory.cs @@ -0,0 +1,29 @@ +using System; +using System.Collections.Generic; +using System.Text; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Design; +using Microsoft.Extensions.Configuration; + +namespace OpenBudgeteer.Core.Common.Database +{ + public class SqliteDatabaseContextFactory : IDesignTimeDbContextFactory + { + public SqliteDatabaseContext CreateDbContext(string[] args) + { + var optionsBuilder = new DbContextOptionsBuilder(); + optionsBuilder.UseSqlite("Data Source=openbudgeteer.db"); + + return new SqliteDatabaseContext(optionsBuilder.Options); + } + + public SqliteDatabaseContext CreateDbContext(string connectionString) + { + var optionsBuilder = new DbContextOptionsBuilder() + .UseSqlite( + connectionString, + b => b.MigrationsAssembly("OpenBudgeteer.Core")); + return new SqliteDatabaseContext(optionsBuilder.Options); + } + } +} diff --git a/OpenBudgeteer.Core/Common/ViewModelOperationResult.cs b/OpenBudgeteer.Core/Common/ViewModelOperationResult.cs new file mode 100644 index 0000000..1683284 --- /dev/null +++ b/OpenBudgeteer.Core/Common/ViewModelOperationResult.cs @@ -0,0 +1,29 @@ +using System; +using System.Collections.Generic; +using System.Text; + +namespace OpenBudgeteer.Core.Common +{ + public class ViewModelOperationResult + { + public bool IsSuccessful { get; } + public string Message { get; } + public bool ViewModelReloadInvoked { get; } + + public ViewModelOperationResult(bool isSuccessful, string message, bool viewModelReloadInvoked = false) + { + IsSuccessful = isSuccessful; + Message = message; + ViewModelReloadInvoked = viewModelReloadInvoked; + } + + public ViewModelOperationResult(bool isSuccessful, bool viewModelReloadInvoked = false) + : this(isSuccessful, string.Empty, viewModelReloadInvoked) + { + if (!isSuccessful) + { + Message = "Unknown Error."; + } + } + } +} diff --git a/OpenBudgeteer.Blazor/Migrations/20200605093534_InitialCreate.Designer.cs b/OpenBudgeteer.Core/Migrations/MySql/20200605093534_InitialCreate.Designer.cs similarity index 98% rename from OpenBudgeteer.Blazor/Migrations/20200605093534_InitialCreate.Designer.cs rename to OpenBudgeteer.Core/Migrations/MySql/20200605093534_InitialCreate.Designer.cs index f395e9c..7be612b 100644 --- a/OpenBudgeteer.Blazor/Migrations/20200605093534_InitialCreate.Designer.cs +++ b/OpenBudgeteer.Core/Migrations/MySql/20200605093534_InitialCreate.Designer.cs @@ -3,11 +3,11 @@ using Microsoft.EntityFrameworkCore.Infrastructure; using Microsoft.EntityFrameworkCore.Migrations; using Microsoft.EntityFrameworkCore.Storage.ValueConversion; -using OpenBudgeteer.Core.Common; +using OpenBudgeteer.Core.Common.Database; -namespace OpenBudgeteer.Blazor.Migrations +namespace OpenBudgeteer.Core.Migrations.MySql { - [DbContext(typeof(DatabaseContext))] + [DbContext(typeof(MySqlDatabaseContext))] [Migration("20200605093534_InitialCreate")] partial class InitialCreate { diff --git a/OpenBudgeteer.Blazor/Migrations/20200605093534_InitialCreate.cs b/OpenBudgeteer.Core/Migrations/MySql/20200605093534_InitialCreate.cs similarity index 99% rename from OpenBudgeteer.Blazor/Migrations/20200605093534_InitialCreate.cs rename to OpenBudgeteer.Core/Migrations/MySql/20200605093534_InitialCreate.cs index a6708c8..c3a287d 100644 --- a/OpenBudgeteer.Blazor/Migrations/20200605093534_InitialCreate.cs +++ b/OpenBudgeteer.Core/Migrations/MySql/20200605093534_InitialCreate.cs @@ -1,7 +1,7 @@ using Microsoft.EntityFrameworkCore.Metadata; using Microsoft.EntityFrameworkCore.Migrations; -namespace OpenBudgeteer.Blazor.Migrations +namespace OpenBudgeteer.Core.Migrations.MySql { public partial class InitialCreate : Migration { diff --git a/OpenBudgeteer.Blazor/Migrations/20200608152707_DecimalPrecision.Designer.cs b/OpenBudgeteer.Core/Migrations/MySql/20200608152707_DecimalPrecision.Designer.cs similarity index 98% rename from OpenBudgeteer.Blazor/Migrations/20200608152707_DecimalPrecision.Designer.cs rename to OpenBudgeteer.Core/Migrations/MySql/20200608152707_DecimalPrecision.Designer.cs index 59b4f87..befe5d3 100644 --- a/OpenBudgeteer.Blazor/Migrations/20200608152707_DecimalPrecision.Designer.cs +++ b/OpenBudgeteer.Core/Migrations/MySql/20200608152707_DecimalPrecision.Designer.cs @@ -3,11 +3,11 @@ using Microsoft.EntityFrameworkCore.Infrastructure; using Microsoft.EntityFrameworkCore.Migrations; using Microsoft.EntityFrameworkCore.Storage.ValueConversion; -using OpenBudgeteer.Core.Common; +using OpenBudgeteer.Core.Common.Database; -namespace OpenBudgeteer.Blazor.Migrations +namespace OpenBudgeteer.Core.Migrations.MySql { - [DbContext(typeof(DatabaseContext))] + [DbContext(typeof(MySqlDatabaseContext))] [Migration("20200608152707_DecimalPrecision")] partial class DecimalPrecision { diff --git a/OpenBudgeteer.Blazor/Migrations/20200608152707_DecimalPrecision.cs b/OpenBudgeteer.Core/Migrations/MySql/20200608152707_DecimalPrecision.cs similarity index 98% rename from OpenBudgeteer.Blazor/Migrations/20200608152707_DecimalPrecision.cs rename to OpenBudgeteer.Core/Migrations/MySql/20200608152707_DecimalPrecision.cs index adb8f7d..2b7ec94 100644 --- a/OpenBudgeteer.Blazor/Migrations/20200608152707_DecimalPrecision.cs +++ b/OpenBudgeteer.Core/Migrations/MySql/20200608152707_DecimalPrecision.cs @@ -1,6 +1,6 @@ using Microsoft.EntityFrameworkCore.Migrations; -namespace OpenBudgeteer.Blazor.Migrations +namespace OpenBudgeteer.Core.Migrations.MySql { public partial class DecimalPrecision : Migration { diff --git a/OpenBudgeteer.Blazor/Migrations/20200612082229_FixedBucketVersion.Designer.cs b/OpenBudgeteer.Core/Migrations/MySql/20200612082229_FixedBucketVersion.Designer.cs similarity index 98% rename from OpenBudgeteer.Blazor/Migrations/20200612082229_FixedBucketVersion.Designer.cs rename to OpenBudgeteer.Core/Migrations/MySql/20200612082229_FixedBucketVersion.Designer.cs index 6649280..19e7e36 100644 --- a/OpenBudgeteer.Blazor/Migrations/20200612082229_FixedBucketVersion.Designer.cs +++ b/OpenBudgeteer.Core/Migrations/MySql/20200612082229_FixedBucketVersion.Designer.cs @@ -3,11 +3,11 @@ using Microsoft.EntityFrameworkCore.Infrastructure; using Microsoft.EntityFrameworkCore.Migrations; using Microsoft.EntityFrameworkCore.Storage.ValueConversion; -using OpenBudgeteer.Core.Common; +using OpenBudgeteer.Core.Common.Database; -namespace OpenBudgeteer.Blazor.Migrations +namespace OpenBudgeteer.Core.Migrations.MySql { - [DbContext(typeof(DatabaseContext))] + [DbContext(typeof(MySqlDatabaseContext))] [Migration("20200612082229_FixedBucketVersion")] partial class FixedBucketVersion { diff --git a/OpenBudgeteer.Blazor/Migrations/20200612082229_FixedBucketVersion.cs b/OpenBudgeteer.Core/Migrations/MySql/20200612082229_FixedBucketVersion.cs similarity index 92% rename from OpenBudgeteer.Blazor/Migrations/20200612082229_FixedBucketVersion.cs rename to OpenBudgeteer.Core/Migrations/MySql/20200612082229_FixedBucketVersion.cs index 4c802cb..ae57003 100644 --- a/OpenBudgeteer.Blazor/Migrations/20200612082229_FixedBucketVersion.cs +++ b/OpenBudgeteer.Core/Migrations/MySql/20200612082229_FixedBucketVersion.cs @@ -1,6 +1,6 @@ using Microsoft.EntityFrameworkCore.Migrations; -namespace OpenBudgeteer.Blazor.Migrations +namespace OpenBudgeteer.Core.Migrations.MySql { public partial class FixedBucketVersion : Migration { diff --git a/OpenBudgeteer.Blazor/Migrations/20200622121904_DefaultIncomeTransferBucket.Designer.cs b/OpenBudgeteer.Core/Migrations/MySql/20200622121904_DefaultIncomeTransferBucket.Designer.cs similarity index 98% rename from OpenBudgeteer.Blazor/Migrations/20200622121904_DefaultIncomeTransferBucket.Designer.cs rename to OpenBudgeteer.Core/Migrations/MySql/20200622121904_DefaultIncomeTransferBucket.Designer.cs index d7d6e78..31f4054 100644 --- a/OpenBudgeteer.Blazor/Migrations/20200622121904_DefaultIncomeTransferBucket.Designer.cs +++ b/OpenBudgeteer.Core/Migrations/MySql/20200622121904_DefaultIncomeTransferBucket.Designer.cs @@ -3,11 +3,11 @@ using Microsoft.EntityFrameworkCore.Infrastructure; using Microsoft.EntityFrameworkCore.Migrations; using Microsoft.EntityFrameworkCore.Storage.ValueConversion; -using OpenBudgeteer.Core.Common; +using OpenBudgeteer.Core.Common.Database; -namespace OpenBudgeteer.Blazor.Migrations +namespace OpenBudgeteer.Core.Migrations.MySql { - [DbContext(typeof(DatabaseContext))] + [DbContext(typeof(MySqlDatabaseContext))] [Migration("20200622121904_DefaultIncomeTransferBucket")] partial class DefaultIncomeTransferBucket { diff --git a/OpenBudgeteer.Blazor/Migrations/20200622121904_DefaultIncomeTransferBucket.cs b/OpenBudgeteer.Core/Migrations/MySql/20200622121904_DefaultIncomeTransferBucket.cs similarity index 96% rename from OpenBudgeteer.Blazor/Migrations/20200622121904_DefaultIncomeTransferBucket.cs rename to OpenBudgeteer.Core/Migrations/MySql/20200622121904_DefaultIncomeTransferBucket.cs index dafb128..c479046 100644 --- a/OpenBudgeteer.Blazor/Migrations/20200622121904_DefaultIncomeTransferBucket.cs +++ b/OpenBudgeteer.Core/Migrations/MySql/20200622121904_DefaultIncomeTransferBucket.cs @@ -1,6 +1,6 @@ using Microsoft.EntityFrameworkCore.Migrations; -namespace OpenBudgeteer.Blazor.Migrations +namespace OpenBudgeteer.Core.Migrations.MySql { public partial class DefaultIncomeTransferBucket : Migration { diff --git a/OpenBudgeteer.Blazor/Migrations/20200701071320_BucketColor.Designer.cs b/OpenBudgeteer.Core/Migrations/MySql/20200701071320_BucketColor.Designer.cs similarity index 98% rename from OpenBudgeteer.Blazor/Migrations/20200701071320_BucketColor.Designer.cs rename to OpenBudgeteer.Core/Migrations/MySql/20200701071320_BucketColor.Designer.cs index b4b93be..e7cb42f 100644 --- a/OpenBudgeteer.Blazor/Migrations/20200701071320_BucketColor.Designer.cs +++ b/OpenBudgeteer.Core/Migrations/MySql/20200701071320_BucketColor.Designer.cs @@ -3,11 +3,11 @@ using Microsoft.EntityFrameworkCore.Infrastructure; using Microsoft.EntityFrameworkCore.Migrations; using Microsoft.EntityFrameworkCore.Storage.ValueConversion; -using OpenBudgeteer.Core.Common; +using OpenBudgeteer.Core.Common.Database; -namespace OpenBudgeteer.Blazor.Migrations +namespace OpenBudgeteer.Core.Migrations.MySql { - [DbContext(typeof(DatabaseContext))] + [DbContext(typeof(MySqlDatabaseContext))] [Migration("20200701071320_BucketColor")] partial class BucketColor { diff --git a/OpenBudgeteer.Blazor/Migrations/20200701071320_BucketColor.cs b/OpenBudgeteer.Core/Migrations/MySql/20200701071320_BucketColor.cs similarity index 92% rename from OpenBudgeteer.Blazor/Migrations/20200701071320_BucketColor.cs rename to OpenBudgeteer.Core/Migrations/MySql/20200701071320_BucketColor.cs index dce4a5b..f98dd44 100644 --- a/OpenBudgeteer.Blazor/Migrations/20200701071320_BucketColor.cs +++ b/OpenBudgeteer.Core/Migrations/MySql/20200701071320_BucketColor.cs @@ -1,6 +1,6 @@ using Microsoft.EntityFrameworkCore.Migrations; -namespace OpenBudgeteer.Blazor.Migrations +namespace OpenBudgeteer.Core.Migrations.MySql { public partial class BucketColor : Migration { diff --git a/OpenBudgeteer.Blazor/Migrations/20200701133458_DateTimeDataType.Designer.cs b/OpenBudgeteer.Core/Migrations/MySql/20200701133458_DateTimeDataType.Designer.cs similarity index 98% rename from OpenBudgeteer.Blazor/Migrations/20200701133458_DateTimeDataType.Designer.cs rename to OpenBudgeteer.Core/Migrations/MySql/20200701133458_DateTimeDataType.Designer.cs index 7478940..7d355c2 100644 --- a/OpenBudgeteer.Blazor/Migrations/20200701133458_DateTimeDataType.Designer.cs +++ b/OpenBudgeteer.Core/Migrations/MySql/20200701133458_DateTimeDataType.Designer.cs @@ -4,11 +4,11 @@ using Microsoft.EntityFrameworkCore.Infrastructure; using Microsoft.EntityFrameworkCore.Migrations; using Microsoft.EntityFrameworkCore.Storage.ValueConversion; -using OpenBudgeteer.Core.Common; +using OpenBudgeteer.Core.Common.Database; -namespace OpenBudgeteer.Blazor.Migrations +namespace OpenBudgeteer.Core.Migrations.MySql { - [DbContext(typeof(DatabaseContext))] + [DbContext(typeof(MySqlDatabaseContext))] [Migration("20200701133458_DateTimeDataType")] partial class DateTimeDataType { diff --git a/OpenBudgeteer.Blazor/Migrations/20200701133458_DateTimeDataType.cs b/OpenBudgeteer.Core/Migrations/MySql/20200701133458_DateTimeDataType.cs similarity index 98% rename from OpenBudgeteer.Blazor/Migrations/20200701133458_DateTimeDataType.cs rename to OpenBudgeteer.Core/Migrations/MySql/20200701133458_DateTimeDataType.cs index c556640..c2769fd 100644 --- a/OpenBudgeteer.Blazor/Migrations/20200701133458_DateTimeDataType.cs +++ b/OpenBudgeteer.Core/Migrations/MySql/20200701133458_DateTimeDataType.cs @@ -1,7 +1,7 @@ using System; using Microsoft.EntityFrameworkCore.Migrations; -namespace OpenBudgeteer.Blazor.Migrations +namespace OpenBudgeteer.Core.Migrations.MySql { public partial class DateTimeDataType : Migration { diff --git a/OpenBudgeteer.Blazor/Migrations/20200704081215_ImportProfileDelimiter.Designer.cs b/OpenBudgeteer.Core/Migrations/MySql/20200704081215_ImportProfileDelimiter.Designer.cs similarity index 98% rename from OpenBudgeteer.Blazor/Migrations/20200704081215_ImportProfileDelimiter.Designer.cs rename to OpenBudgeteer.Core/Migrations/MySql/20200704081215_ImportProfileDelimiter.Designer.cs index 9498cd5..a8910bf 100644 --- a/OpenBudgeteer.Blazor/Migrations/20200704081215_ImportProfileDelimiter.Designer.cs +++ b/OpenBudgeteer.Core/Migrations/MySql/20200704081215_ImportProfileDelimiter.Designer.cs @@ -4,11 +4,11 @@ using Microsoft.EntityFrameworkCore.Infrastructure; using Microsoft.EntityFrameworkCore.Migrations; using Microsoft.EntityFrameworkCore.Storage.ValueConversion; -using OpenBudgeteer.Core.Common; +using OpenBudgeteer.Core.Common.Database; -namespace OpenBudgeteer.Blazor.Migrations +namespace OpenBudgeteer.Core.Migrations.MySql { - [DbContext(typeof(DatabaseContext))] + [DbContext(typeof(MySqlDatabaseContext))] [Migration("20200704081215_ImportProfileDelimiter")] partial class ImportProfileDelimiter { diff --git a/OpenBudgeteer.Blazor/Migrations/20200704081215_ImportProfileDelimiter.cs b/OpenBudgeteer.Core/Migrations/MySql/20200704081215_ImportProfileDelimiter.cs similarity index 95% rename from OpenBudgeteer.Blazor/Migrations/20200704081215_ImportProfileDelimiter.cs rename to OpenBudgeteer.Core/Migrations/MySql/20200704081215_ImportProfileDelimiter.cs index 0878ef8..d4f36bc 100644 --- a/OpenBudgeteer.Blazor/Migrations/20200704081215_ImportProfileDelimiter.cs +++ b/OpenBudgeteer.Core/Migrations/MySql/20200704081215_ImportProfileDelimiter.cs @@ -1,6 +1,6 @@ using Microsoft.EntityFrameworkCore.Migrations; -namespace OpenBudgeteer.Blazor.Migrations +namespace OpenBudgeteer.Core.Migrations.MySql { public partial class ImportProfileDelimiter : Migration { diff --git a/OpenBudgeteer.Blazor/Migrations/20200707141613_ImportProfileDateNumberFormat.Designer.cs b/OpenBudgeteer.Core/Migrations/MySql/20200707141613_ImportProfileDateNumberFormat.Designer.cs similarity index 98% rename from OpenBudgeteer.Blazor/Migrations/20200707141613_ImportProfileDateNumberFormat.Designer.cs rename to OpenBudgeteer.Core/Migrations/MySql/20200707141613_ImportProfileDateNumberFormat.Designer.cs index 9812635..4fa1438 100644 --- a/OpenBudgeteer.Blazor/Migrations/20200707141613_ImportProfileDateNumberFormat.Designer.cs +++ b/OpenBudgeteer.Core/Migrations/MySql/20200707141613_ImportProfileDateNumberFormat.Designer.cs @@ -4,11 +4,11 @@ using Microsoft.EntityFrameworkCore.Infrastructure; using Microsoft.EntityFrameworkCore.Migrations; using Microsoft.EntityFrameworkCore.Storage.ValueConversion; -using OpenBudgeteer.Core.Common; +using OpenBudgeteer.Core.Common.Database; -namespace OpenBudgeteer.Blazor.Migrations +namespace OpenBudgeteer.Core.Migrations.MySql { - [DbContext(typeof(DatabaseContext))] + [DbContext(typeof(MySqlDatabaseContext))] [Migration("20200707141613_ImportProfileDateNumberFormat")] partial class ImportProfileDateNumberFormat { diff --git a/OpenBudgeteer.Blazor/Migrations/20200707141613_ImportProfileDateNumberFormat.cs b/OpenBudgeteer.Core/Migrations/MySql/20200707141613_ImportProfileDateNumberFormat.cs similarity index 95% rename from OpenBudgeteer.Blazor/Migrations/20200707141613_ImportProfileDateNumberFormat.cs rename to OpenBudgeteer.Core/Migrations/MySql/20200707141613_ImportProfileDateNumberFormat.cs index e836b78..9369046 100644 --- a/OpenBudgeteer.Blazor/Migrations/20200707141613_ImportProfileDateNumberFormat.cs +++ b/OpenBudgeteer.Core/Migrations/MySql/20200707141613_ImportProfileDateNumberFormat.cs @@ -1,6 +1,6 @@ using Microsoft.EntityFrameworkCore.Migrations; -namespace OpenBudgeteer.Blazor.Migrations +namespace OpenBudgeteer.Core.Migrations.MySql { public partial class ImportProfileDateNumberFormat : Migration { diff --git a/OpenBudgeteer.Blazor/Migrations/20200723111131_BucketNotes.Designer.cs b/OpenBudgeteer.Core/Migrations/MySql/20200723111131_BucketNotes.Designer.cs similarity index 98% rename from OpenBudgeteer.Blazor/Migrations/20200723111131_BucketNotes.Designer.cs rename to OpenBudgeteer.Core/Migrations/MySql/20200723111131_BucketNotes.Designer.cs index 98aa4d7..6c394c9 100644 --- a/OpenBudgeteer.Blazor/Migrations/20200723111131_BucketNotes.Designer.cs +++ b/OpenBudgeteer.Core/Migrations/MySql/20200723111131_BucketNotes.Designer.cs @@ -4,11 +4,11 @@ using Microsoft.EntityFrameworkCore.Infrastructure; using Microsoft.EntityFrameworkCore.Migrations; using Microsoft.EntityFrameworkCore.Storage.ValueConversion; -using OpenBudgeteer.Core.Common; +using OpenBudgeteer.Core.Common.Database; -namespace OpenBudgeteer.Blazor.Migrations +namespace OpenBudgeteer.Core.Migrations.MySql { - [DbContext(typeof(DatabaseContext))] + [DbContext(typeof(MySqlDatabaseContext))] [Migration("20200723111131_BucketNotes")] partial class BucketNotes { diff --git a/OpenBudgeteer.Blazor/Migrations/20200723111131_BucketNotes.cs b/OpenBudgeteer.Core/Migrations/MySql/20200723111131_BucketNotes.cs similarity index 92% rename from OpenBudgeteer.Blazor/Migrations/20200723111131_BucketNotes.cs rename to OpenBudgeteer.Core/Migrations/MySql/20200723111131_BucketNotes.cs index 165f662..2f00d2c 100644 --- a/OpenBudgeteer.Blazor/Migrations/20200723111131_BucketNotes.cs +++ b/OpenBudgeteer.Core/Migrations/MySql/20200723111131_BucketNotes.cs @@ -1,6 +1,6 @@ using Microsoft.EntityFrameworkCore.Migrations; -namespace OpenBudgeteer.Blazor.Migrations +namespace OpenBudgeteer.Core.Migrations.MySql { public partial class BucketNotes : Migration { diff --git a/OpenBudgeteer.Blazor/Migrations/20200822152402_AutomaticBucketAssignment.Designer.cs b/OpenBudgeteer.Core/Migrations/MySql/20200822152402_AutomaticBucketAssignment.Designer.cs similarity index 98% rename from OpenBudgeteer.Blazor/Migrations/20200822152402_AutomaticBucketAssignment.Designer.cs rename to OpenBudgeteer.Core/Migrations/MySql/20200822152402_AutomaticBucketAssignment.Designer.cs index 7a30532..4e5596b 100644 --- a/OpenBudgeteer.Blazor/Migrations/20200822152402_AutomaticBucketAssignment.Designer.cs +++ b/OpenBudgeteer.Core/Migrations/MySql/20200822152402_AutomaticBucketAssignment.Designer.cs @@ -4,11 +4,11 @@ using Microsoft.EntityFrameworkCore.Infrastructure; using Microsoft.EntityFrameworkCore.Migrations; using Microsoft.EntityFrameworkCore.Storage.ValueConversion; -using OpenBudgeteer.Core.Common; +using OpenBudgeteer.Core.Common.Database; -namespace OpenBudgeteer.Blazor.Migrations +namespace OpenBudgeteer.Core.Migrations.MySql { - [DbContext(typeof(DatabaseContext))] + [DbContext(typeof(MySqlDatabaseContext))] [Migration("20200822152402_AutomaticBucketAssignment")] partial class AutomaticBucketAssignment { diff --git a/OpenBudgeteer.Blazor/Migrations/20200822152402_AutomaticBucketAssignment.cs b/OpenBudgeteer.Core/Migrations/MySql/20200822152402_AutomaticBucketAssignment.cs similarity index 97% rename from OpenBudgeteer.Blazor/Migrations/20200822152402_AutomaticBucketAssignment.cs rename to OpenBudgeteer.Core/Migrations/MySql/20200822152402_AutomaticBucketAssignment.cs index 4d24fe1..84b8ab5 100644 --- a/OpenBudgeteer.Blazor/Migrations/20200822152402_AutomaticBucketAssignment.cs +++ b/OpenBudgeteer.Core/Migrations/MySql/20200822152402_AutomaticBucketAssignment.cs @@ -1,7 +1,7 @@ using Microsoft.EntityFrameworkCore.Metadata; using Microsoft.EntityFrameworkCore.Migrations; -namespace OpenBudgeteer.Blazor.Migrations +namespace OpenBudgeteer.Core.Migrations.MySql { public partial class AutomaticBucketAssignment : Migration { diff --git a/OpenBudgeteer.Blazor/Migrations/DatabaseServiceModelSnapshot.cs b/OpenBudgeteer.Core/Migrations/MySql/DatabaseServiceModelSnapshot.cs similarity index 98% rename from OpenBudgeteer.Blazor/Migrations/DatabaseServiceModelSnapshot.cs rename to OpenBudgeteer.Core/Migrations/MySql/DatabaseServiceModelSnapshot.cs index b3deeb4..ada9233 100644 --- a/OpenBudgeteer.Blazor/Migrations/DatabaseServiceModelSnapshot.cs +++ b/OpenBudgeteer.Core/Migrations/MySql/DatabaseServiceModelSnapshot.cs @@ -3,11 +3,11 @@ using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore.Infrastructure; using Microsoft.EntityFrameworkCore.Storage.ValueConversion; -using OpenBudgeteer.Core.Common; +using OpenBudgeteer.Core.Common.Database; -namespace OpenBudgeteer.Blazor.Migrations +namespace OpenBudgeteer.Core.Migrations.MySql { - [DbContext(typeof(DatabaseContext))] + [DbContext(typeof(MySqlDatabaseContext))] partial class DatabaseServiceModelSnapshot : ModelSnapshot { protected override void BuildModel(ModelBuilder modelBuilder) diff --git a/OpenBudgeteer.Core/Migrations/Sqlite/20200925091603_InitialCreate.Designer.cs b/OpenBudgeteer.Core/Migrations/Sqlite/20200925091603_InitialCreate.Designer.cs new file mode 100644 index 0000000..f6da2a4 --- /dev/null +++ b/OpenBudgeteer.Core/Migrations/Sqlite/20200925091603_InitialCreate.Designer.cs @@ -0,0 +1,275 @@ +// +using System; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; +using OpenBudgeteer.Core.Common.Database; + +namespace OpenBudgeteer.Core.Migrations.Sqlite +{ + [DbContext(typeof(SqliteDatabaseContext))] + [Migration("20200925091603_InitialCreate")] + partial class InitialCreate + { + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasAnnotation("ProductVersion", "3.1.8"); + + modelBuilder.Entity("OpenBudgeteer.Core.Models.Account", b => + { + b.Property("AccountId") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("IsActive") + .HasColumnType("INTEGER"); + + b.Property("Name") + .HasColumnType("TEXT"); + + b.HasKey("AccountId"); + + b.ToTable("Account"); + }); + + modelBuilder.Entity("OpenBudgeteer.Core.Models.BankTransaction", b => + { + b.Property("TransactionId") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("AccountId") + .HasColumnType("INTEGER"); + + b.Property("Amount") + .HasColumnType("decimal(65, 2)"); + + b.Property("Memo") + .HasColumnType("TEXT"); + + b.Property("Payee") + .HasColumnType("TEXT"); + + b.Property("TransactionDate") + .HasColumnType("TEXT"); + + b.HasKey("TransactionId"); + + b.ToTable("BankTransaction"); + }); + + modelBuilder.Entity("OpenBudgeteer.Core.Models.Bucket", b => + { + b.Property("BucketId") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("BucketGroupId") + .HasColumnType("INTEGER"); + + b.Property("ColorCode") + .HasColumnType("TEXT"); + + b.Property("IsInactive") + .HasColumnType("INTEGER"); + + b.Property("IsInactiveFrom") + .HasColumnType("TEXT"); + + b.Property("Name") + .HasColumnType("TEXT"); + + b.Property("ValidFrom") + .HasColumnType("TEXT"); + + b.HasKey("BucketId"); + + b.ToTable("Bucket"); + }); + + modelBuilder.Entity("OpenBudgeteer.Core.Models.BucketGroup", b => + { + b.Property("BucketGroupId") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("Name") + .HasColumnType("TEXT"); + + b.Property("Position") + .HasColumnType("INTEGER"); + + b.HasKey("BucketGroupId"); + + b.ToTable("BucketGroup"); + }); + + modelBuilder.Entity("OpenBudgeteer.Core.Models.BucketMovement", b => + { + b.Property("BucketMovementId") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("Amount") + .HasColumnType("decimal(65, 2)"); + + b.Property("BucketId") + .HasColumnType("INTEGER"); + + b.Property("MovementDate") + .HasColumnType("TEXT"); + + b.HasKey("BucketMovementId"); + + b.ToTable("BucketMovement"); + }); + + modelBuilder.Entity("OpenBudgeteer.Core.Models.BucketRuleSet", b => + { + b.Property("BucketRuleSetId") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("Name") + .HasColumnType("TEXT"); + + b.Property("Priority") + .HasColumnType("INTEGER"); + + b.Property("TargetBucketId") + .HasColumnType("INTEGER"); + + b.HasKey("BucketRuleSetId"); + + b.ToTable("BucketRuleSet"); + }); + + modelBuilder.Entity("OpenBudgeteer.Core.Models.BucketVersion", b => + { + b.Property("BucketVersionId") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("BucketId") + .HasColumnType("INTEGER"); + + b.Property("BucketType") + .HasColumnType("INTEGER"); + + b.Property("BucketTypeXParam") + .HasColumnType("INTEGER"); + + b.Property("BucketTypeYParam") + .HasColumnType("decimal(65, 2)"); + + b.Property("BucketTypeZParam") + .HasColumnType("TEXT"); + + b.Property("Notes") + .HasColumnType("TEXT"); + + b.Property("ValidFrom") + .HasColumnType("TEXT"); + + b.Property("Version") + .HasColumnType("INTEGER"); + + b.HasKey("BucketVersionId"); + + b.ToTable("BucketVersion"); + }); + + modelBuilder.Entity("OpenBudgeteer.Core.Models.BudgetedTransaction", b => + { + b.Property("BudgetedTransactionId") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("Amount") + .HasColumnType("decimal(65, 2)"); + + b.Property("BucketId") + .HasColumnType("INTEGER"); + + b.Property("TransactionId") + .HasColumnType("INTEGER"); + + b.HasKey("BudgetedTransactionId"); + + b.ToTable("BudgetedTransaction"); + }); + + modelBuilder.Entity("OpenBudgeteer.Core.Models.ImportProfile", b => + { + b.Property("ImportProfileId") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("AccountId") + .HasColumnType("INTEGER"); + + b.Property("AmountColumnName") + .HasColumnType("TEXT"); + + b.Property("DateFormat") + .HasColumnType("TEXT"); + + b.Property("Delimiter") + .HasColumnType("TEXT"); + + b.Property("HeaderRow") + .HasColumnType("INTEGER"); + + b.Property("MemoColumnName") + .HasColumnType("TEXT"); + + b.Property("NumberFormat") + .HasColumnType("TEXT"); + + b.Property("PayeeColumnName") + .HasColumnType("TEXT"); + + b.Property("ProfileName") + .HasColumnType("TEXT"); + + b.Property("TextQualifier") + .HasColumnType("TEXT"); + + b.Property("TransactionDateColumnName") + .HasColumnType("TEXT"); + + b.HasKey("ImportProfileId"); + + b.ToTable("ImportProfile"); + }); + + modelBuilder.Entity("OpenBudgeteer.Core.Models.MappingRule", b => + { + b.Property("MappingRuleId") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("BucketRuleSetId") + .HasColumnType("INTEGER"); + + b.Property("ComparisionField") + .HasColumnType("INTEGER"); + + b.Property("ComparisionType") + .HasColumnType("INTEGER"); + + b.Property("ComparisionValue") + .IsRequired() + .HasColumnType("TEXT"); + + b.HasKey("MappingRuleId"); + + b.ToTable("MappingRule"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/OpenBudgeteer.Core/Migrations/Sqlite/20200925091603_InitialCreate.cs b/OpenBudgeteer.Core/Migrations/Sqlite/20200925091603_InitialCreate.cs new file mode 100644 index 0000000..ccadfed --- /dev/null +++ b/OpenBudgeteer.Core/Migrations/Sqlite/20200925091603_InitialCreate.cs @@ -0,0 +1,211 @@ +using System; +using Microsoft.EntityFrameworkCore.Migrations; + +namespace OpenBudgeteer.Core.Migrations.Sqlite +{ + public partial class InitialCreate : Migration + { + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.CreateTable( + name: "Account", + columns: table => new + { + AccountId = table.Column(nullable: false) + .Annotation("Sqlite:Autoincrement", true), + Name = table.Column(nullable: true), + IsActive = table.Column(nullable: false) + }, + constraints: table => + { + table.PrimaryKey("PK_Account", x => x.AccountId); + }); + + migrationBuilder.CreateTable( + name: "BankTransaction", + columns: table => new + { + TransactionId = table.Column(nullable: false) + .Annotation("Sqlite:Autoincrement", true), + AccountId = table.Column(nullable: false), + TransactionDate = table.Column(nullable: false), + Payee = table.Column(nullable: true), + Memo = table.Column(nullable: true), + Amount = table.Column(type: "decimal(65, 2)", nullable: false) + }, + constraints: table => + { + table.PrimaryKey("PK_BankTransaction", x => x.TransactionId); + }); + + migrationBuilder.CreateTable( + name: "Bucket", + columns: table => new + { + BucketId = table.Column(nullable: false) + .Annotation("Sqlite:Autoincrement", true), + Name = table.Column(nullable: true), + BucketGroupId = table.Column(nullable: false), + ColorCode = table.Column(nullable: true), + ValidFrom = table.Column(nullable: false), + IsInactive = table.Column(nullable: false), + IsInactiveFrom = table.Column(nullable: false) + }, + constraints: table => + { + table.PrimaryKey("PK_Bucket", x => x.BucketId); + }); + + migrationBuilder.CreateTable( + name: "BucketGroup", + columns: table => new + { + BucketGroupId = table.Column(nullable: false) + .Annotation("Sqlite:Autoincrement", true), + Name = table.Column(nullable: true), + Position = table.Column(nullable: false) + }, + constraints: table => + { + table.PrimaryKey("PK_BucketGroup", x => x.BucketGroupId); + }); + + migrationBuilder.CreateTable( + name: "BucketMovement", + columns: table => new + { + BucketMovementId = table.Column(nullable: false) + .Annotation("Sqlite:Autoincrement", true), + BucketId = table.Column(nullable: false), + Amount = table.Column(type: "decimal(65, 2)", nullable: false), + MovementDate = table.Column(nullable: false) + }, + constraints: table => + { + table.PrimaryKey("PK_BucketMovement", x => x.BucketMovementId); + }); + + migrationBuilder.CreateTable( + name: "BucketRuleSet", + columns: table => new + { + BucketRuleSetId = table.Column(nullable: false) + .Annotation("Sqlite:Autoincrement", true), + Priority = table.Column(nullable: false), + Name = table.Column(nullable: true), + TargetBucketId = table.Column(nullable: false) + }, + constraints: table => + { + table.PrimaryKey("PK_BucketRuleSet", x => x.BucketRuleSetId); + }); + + migrationBuilder.CreateTable( + name: "BucketVersion", + columns: table => new + { + BucketVersionId = table.Column(nullable: false) + .Annotation("Sqlite:Autoincrement", true), + BucketId = table.Column(nullable: false), + Version = table.Column(nullable: false), + BucketType = table.Column(nullable: false), + BucketTypeXParam = table.Column(nullable: false), + BucketTypeYParam = table.Column(type: "decimal(65, 2)", nullable: false), + BucketTypeZParam = table.Column(nullable: false), + Notes = table.Column(nullable: true), + ValidFrom = table.Column(nullable: false) + }, + constraints: table => + { + table.PrimaryKey("PK_BucketVersion", x => x.BucketVersionId); + }); + + migrationBuilder.CreateTable( + name: "BudgetedTransaction", + columns: table => new + { + BudgetedTransactionId = table.Column(nullable: false) + .Annotation("Sqlite:Autoincrement", true), + TransactionId = table.Column(nullable: false), + BucketId = table.Column(nullable: false), + Amount = table.Column(type: "decimal(65, 2)", nullable: false) + }, + constraints: table => + { + table.PrimaryKey("PK_BudgetedTransaction", x => x.BudgetedTransactionId); + }); + + migrationBuilder.CreateTable( + name: "ImportProfile", + columns: table => new + { + ImportProfileId = table.Column(nullable: false) + .Annotation("Sqlite:Autoincrement", true), + ProfileName = table.Column(nullable: true), + AccountId = table.Column(nullable: false), + HeaderRow = table.Column(nullable: false), + Delimiter = table.Column(nullable: false), + TextQualifier = table.Column(nullable: false), + DateFormat = table.Column(nullable: true), + NumberFormat = table.Column(nullable: true), + TransactionDateColumnName = table.Column(nullable: true), + PayeeColumnName = table.Column(nullable: true), + MemoColumnName = table.Column(nullable: true), + AmountColumnName = table.Column(nullable: true) + }, + constraints: table => + { + table.PrimaryKey("PK_ImportProfile", x => x.ImportProfileId); + }); + + migrationBuilder.CreateTable( + name: "MappingRule", + columns: table => new + { + MappingRuleId = table.Column(nullable: false) + .Annotation("Sqlite:Autoincrement", true), + BucketRuleSetId = table.Column(nullable: false), + ComparisionField = table.Column(nullable: false), + ComparisionType = table.Column(nullable: false), + ComparisionValue = table.Column(nullable: false) + }, + constraints: table => + { + table.PrimaryKey("PK_MappingRule", x => x.MappingRuleId); + }); + } + + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropTable( + name: "Account"); + + migrationBuilder.DropTable( + name: "BankTransaction"); + + migrationBuilder.DropTable( + name: "Bucket"); + + migrationBuilder.DropTable( + name: "BucketGroup"); + + migrationBuilder.DropTable( + name: "BucketMovement"); + + migrationBuilder.DropTable( + name: "BucketRuleSet"); + + migrationBuilder.DropTable( + name: "BucketVersion"); + + migrationBuilder.DropTable( + name: "BudgetedTransaction"); + + migrationBuilder.DropTable( + name: "ImportProfile"); + + migrationBuilder.DropTable( + name: "MappingRule"); + } + } +} diff --git a/OpenBudgeteer.Core/Migrations/Sqlite/20200925091907_AddInitialRecords.Designer.cs b/OpenBudgeteer.Core/Migrations/Sqlite/20200925091907_AddInitialRecords.Designer.cs new file mode 100644 index 0000000..e975505 --- /dev/null +++ b/OpenBudgeteer.Core/Migrations/Sqlite/20200925091907_AddInitialRecords.Designer.cs @@ -0,0 +1,275 @@ +// +using System; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; +using OpenBudgeteer.Core.Common.Database; + +namespace OpenBudgeteer.Core.Migrations.Sqlite +{ + [DbContext(typeof(SqliteDatabaseContext))] + [Migration("20200925091907_AddInitialRecords")] + partial class AddInitialRecords + { + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasAnnotation("ProductVersion", "3.1.8"); + + modelBuilder.Entity("OpenBudgeteer.Core.Models.Account", b => + { + b.Property("AccountId") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("IsActive") + .HasColumnType("INTEGER"); + + b.Property("Name") + .HasColumnType("TEXT"); + + b.HasKey("AccountId"); + + b.ToTable("Account"); + }); + + modelBuilder.Entity("OpenBudgeteer.Core.Models.BankTransaction", b => + { + b.Property("TransactionId") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("AccountId") + .HasColumnType("INTEGER"); + + b.Property("Amount") + .HasColumnType("decimal(65, 2)"); + + b.Property("Memo") + .HasColumnType("TEXT"); + + b.Property("Payee") + .HasColumnType("TEXT"); + + b.Property("TransactionDate") + .HasColumnType("TEXT"); + + b.HasKey("TransactionId"); + + b.ToTable("BankTransaction"); + }); + + modelBuilder.Entity("OpenBudgeteer.Core.Models.Bucket", b => + { + b.Property("BucketId") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("BucketGroupId") + .HasColumnType("INTEGER"); + + b.Property("ColorCode") + .HasColumnType("TEXT"); + + b.Property("IsInactive") + .HasColumnType("INTEGER"); + + b.Property("IsInactiveFrom") + .HasColumnType("TEXT"); + + b.Property("Name") + .HasColumnType("TEXT"); + + b.Property("ValidFrom") + .HasColumnType("TEXT"); + + b.HasKey("BucketId"); + + b.ToTable("Bucket"); + }); + + modelBuilder.Entity("OpenBudgeteer.Core.Models.BucketGroup", b => + { + b.Property("BucketGroupId") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("Name") + .HasColumnType("TEXT"); + + b.Property("Position") + .HasColumnType("INTEGER"); + + b.HasKey("BucketGroupId"); + + b.ToTable("BucketGroup"); + }); + + modelBuilder.Entity("OpenBudgeteer.Core.Models.BucketMovement", b => + { + b.Property("BucketMovementId") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("Amount") + .HasColumnType("decimal(65, 2)"); + + b.Property("BucketId") + .HasColumnType("INTEGER"); + + b.Property("MovementDate") + .HasColumnType("TEXT"); + + b.HasKey("BucketMovementId"); + + b.ToTable("BucketMovement"); + }); + + modelBuilder.Entity("OpenBudgeteer.Core.Models.BucketRuleSet", b => + { + b.Property("BucketRuleSetId") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("Name") + .HasColumnType("TEXT"); + + b.Property("Priority") + .HasColumnType("INTEGER"); + + b.Property("TargetBucketId") + .HasColumnType("INTEGER"); + + b.HasKey("BucketRuleSetId"); + + b.ToTable("BucketRuleSet"); + }); + + modelBuilder.Entity("OpenBudgeteer.Core.Models.BucketVersion", b => + { + b.Property("BucketVersionId") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("BucketId") + .HasColumnType("INTEGER"); + + b.Property("BucketType") + .HasColumnType("INTEGER"); + + b.Property("BucketTypeXParam") + .HasColumnType("INTEGER"); + + b.Property("BucketTypeYParam") + .HasColumnType("decimal(65, 2)"); + + b.Property("BucketTypeZParam") + .HasColumnType("TEXT"); + + b.Property("Notes") + .HasColumnType("TEXT"); + + b.Property("ValidFrom") + .HasColumnType("TEXT"); + + b.Property("Version") + .HasColumnType("INTEGER"); + + b.HasKey("BucketVersionId"); + + b.ToTable("BucketVersion"); + }); + + modelBuilder.Entity("OpenBudgeteer.Core.Models.BudgetedTransaction", b => + { + b.Property("BudgetedTransactionId") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("Amount") + .HasColumnType("decimal(65, 2)"); + + b.Property("BucketId") + .HasColumnType("INTEGER"); + + b.Property("TransactionId") + .HasColumnType("INTEGER"); + + b.HasKey("BudgetedTransactionId"); + + b.ToTable("BudgetedTransaction"); + }); + + modelBuilder.Entity("OpenBudgeteer.Core.Models.ImportProfile", b => + { + b.Property("ImportProfileId") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("AccountId") + .HasColumnType("INTEGER"); + + b.Property("AmountColumnName") + .HasColumnType("TEXT"); + + b.Property("DateFormat") + .HasColumnType("TEXT"); + + b.Property("Delimiter") + .HasColumnType("TEXT"); + + b.Property("HeaderRow") + .HasColumnType("INTEGER"); + + b.Property("MemoColumnName") + .HasColumnType("TEXT"); + + b.Property("NumberFormat") + .HasColumnType("TEXT"); + + b.Property("PayeeColumnName") + .HasColumnType("TEXT"); + + b.Property("ProfileName") + .HasColumnType("TEXT"); + + b.Property("TextQualifier") + .HasColumnType("TEXT"); + + b.Property("TransactionDateColumnName") + .HasColumnType("TEXT"); + + b.HasKey("ImportProfileId"); + + b.ToTable("ImportProfile"); + }); + + modelBuilder.Entity("OpenBudgeteer.Core.Models.MappingRule", b => + { + b.Property("MappingRuleId") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("BucketRuleSetId") + .HasColumnType("INTEGER"); + + b.Property("ComparisionField") + .HasColumnType("INTEGER"); + + b.Property("ComparisionType") + .HasColumnType("INTEGER"); + + b.Property("ComparisionValue") + .IsRequired() + .HasColumnType("TEXT"); + + b.HasKey("MappingRuleId"); + + b.ToTable("MappingRule"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/OpenBudgeteer.Core/Migrations/Sqlite/20200925091907_AddInitialRecords.cs b/OpenBudgeteer.Core/Migrations/Sqlite/20200925091907_AddInitialRecords.cs new file mode 100644 index 0000000..f21406a --- /dev/null +++ b/OpenBudgeteer.Core/Migrations/Sqlite/20200925091907_AddInitialRecords.cs @@ -0,0 +1,33 @@ +using System; +using Microsoft.EntityFrameworkCore.Migrations; + +namespace OpenBudgeteer.Core.Migrations.Sqlite +{ + public partial class AddInitialRecords : Migration + { + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.InsertData( + table: "Bucket", + columns: new[] { "BucketId", "Name", "BucketGroupId", "ValidFrom", "IsInactive", "IsInactiveFrom" }, + values: new object[] { 1, "Income", 0, "1990-01-01", false, $"{DateTime.MaxValue:yyyy-MM-dd}" }); + migrationBuilder.InsertData( + table: "Bucket", + columns: new[] { "BucketId", "Name", "BucketGroupId", "ValidFrom", "IsInactive", "IsInactiveFrom" }, + values: new object[] { 2, "Transfer", 0, "1990-01-01", false, $"{DateTime.MaxValue:yyyy-MM-dd}" }); + } + + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DeleteData( + table: "Bucket", + keyColumn: "BucketId", + keyValue: 1); + + migrationBuilder.DeleteData( + table: "Bucket", + keyColumn: "BucketId", + keyValue: 2); + } + } +} diff --git a/OpenBudgeteer.Core/Migrations/Sqlite/SqliteDatabaseContextModelSnapshot.cs b/OpenBudgeteer.Core/Migrations/Sqlite/SqliteDatabaseContextModelSnapshot.cs new file mode 100644 index 0000000..03142f2 --- /dev/null +++ b/OpenBudgeteer.Core/Migrations/Sqlite/SqliteDatabaseContextModelSnapshot.cs @@ -0,0 +1,273 @@ +// +using System; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; +using OpenBudgeteer.Core.Common.Database; + +namespace OpenBudgeteer.Core.Migrations.Sqlite +{ + [DbContext(typeof(SqliteDatabaseContext))] + partial class SqliteDatabaseContextModelSnapshot : ModelSnapshot + { + protected override void BuildModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasAnnotation("ProductVersion", "3.1.8"); + + modelBuilder.Entity("OpenBudgeteer.Core.Models.Account", b => + { + b.Property("AccountId") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("IsActive") + .HasColumnType("INTEGER"); + + b.Property("Name") + .HasColumnType("TEXT"); + + b.HasKey("AccountId"); + + b.ToTable("Account"); + }); + + modelBuilder.Entity("OpenBudgeteer.Core.Models.BankTransaction", b => + { + b.Property("TransactionId") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("AccountId") + .HasColumnType("INTEGER"); + + b.Property("Amount") + .HasColumnType("decimal(65, 2)"); + + b.Property("Memo") + .HasColumnType("TEXT"); + + b.Property("Payee") + .HasColumnType("TEXT"); + + b.Property("TransactionDate") + .HasColumnType("TEXT"); + + b.HasKey("TransactionId"); + + b.ToTable("BankTransaction"); + }); + + modelBuilder.Entity("OpenBudgeteer.Core.Models.Bucket", b => + { + b.Property("BucketId") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("BucketGroupId") + .HasColumnType("INTEGER"); + + b.Property("ColorCode") + .HasColumnType("TEXT"); + + b.Property("IsInactive") + .HasColumnType("INTEGER"); + + b.Property("IsInactiveFrom") + .HasColumnType("TEXT"); + + b.Property("Name") + .HasColumnType("TEXT"); + + b.Property("ValidFrom") + .HasColumnType("TEXT"); + + b.HasKey("BucketId"); + + b.ToTable("Bucket"); + }); + + modelBuilder.Entity("OpenBudgeteer.Core.Models.BucketGroup", b => + { + b.Property("BucketGroupId") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("Name") + .HasColumnType("TEXT"); + + b.Property("Position") + .HasColumnType("INTEGER"); + + b.HasKey("BucketGroupId"); + + b.ToTable("BucketGroup"); + }); + + modelBuilder.Entity("OpenBudgeteer.Core.Models.BucketMovement", b => + { + b.Property("BucketMovementId") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("Amount") + .HasColumnType("decimal(65, 2)"); + + b.Property("BucketId") + .HasColumnType("INTEGER"); + + b.Property("MovementDate") + .HasColumnType("TEXT"); + + b.HasKey("BucketMovementId"); + + b.ToTable("BucketMovement"); + }); + + modelBuilder.Entity("OpenBudgeteer.Core.Models.BucketRuleSet", b => + { + b.Property("BucketRuleSetId") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("Name") + .HasColumnType("TEXT"); + + b.Property("Priority") + .HasColumnType("INTEGER"); + + b.Property("TargetBucketId") + .HasColumnType("INTEGER"); + + b.HasKey("BucketRuleSetId"); + + b.ToTable("BucketRuleSet"); + }); + + modelBuilder.Entity("OpenBudgeteer.Core.Models.BucketVersion", b => + { + b.Property("BucketVersionId") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("BucketId") + .HasColumnType("INTEGER"); + + b.Property("BucketType") + .HasColumnType("INTEGER"); + + b.Property("BucketTypeXParam") + .HasColumnType("INTEGER"); + + b.Property("BucketTypeYParam") + .HasColumnType("decimal(65, 2)"); + + b.Property("BucketTypeZParam") + .HasColumnType("TEXT"); + + b.Property("Notes") + .HasColumnType("TEXT"); + + b.Property("ValidFrom") + .HasColumnType("TEXT"); + + b.Property("Version") + .HasColumnType("INTEGER"); + + b.HasKey("BucketVersionId"); + + b.ToTable("BucketVersion"); + }); + + modelBuilder.Entity("OpenBudgeteer.Core.Models.BudgetedTransaction", b => + { + b.Property("BudgetedTransactionId") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("Amount") + .HasColumnType("decimal(65, 2)"); + + b.Property("BucketId") + .HasColumnType("INTEGER"); + + b.Property("TransactionId") + .HasColumnType("INTEGER"); + + b.HasKey("BudgetedTransactionId"); + + b.ToTable("BudgetedTransaction"); + }); + + modelBuilder.Entity("OpenBudgeteer.Core.Models.ImportProfile", b => + { + b.Property("ImportProfileId") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("AccountId") + .HasColumnType("INTEGER"); + + b.Property("AmountColumnName") + .HasColumnType("TEXT"); + + b.Property("DateFormat") + .HasColumnType("TEXT"); + + b.Property("Delimiter") + .HasColumnType("TEXT"); + + b.Property("HeaderRow") + .HasColumnType("INTEGER"); + + b.Property("MemoColumnName") + .HasColumnType("TEXT"); + + b.Property("NumberFormat") + .HasColumnType("TEXT"); + + b.Property("PayeeColumnName") + .HasColumnType("TEXT"); + + b.Property("ProfileName") + .HasColumnType("TEXT"); + + b.Property("TextQualifier") + .HasColumnType("TEXT"); + + b.Property("TransactionDateColumnName") + .HasColumnType("TEXT"); + + b.HasKey("ImportProfileId"); + + b.ToTable("ImportProfile"); + }); + + modelBuilder.Entity("OpenBudgeteer.Core.Models.MappingRule", b => + { + b.Property("MappingRuleId") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("BucketRuleSetId") + .HasColumnType("INTEGER"); + + b.Property("ComparisionField") + .HasColumnType("INTEGER"); + + b.Property("ComparisionType") + .HasColumnType("INTEGER"); + + b.Property("ComparisionValue") + .IsRequired() + .HasColumnType("TEXT"); + + b.HasKey("MappingRuleId"); + + b.ToTable("MappingRule"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/OpenBudgeteer.Core/Models/BucketVersion.cs b/OpenBudgeteer.Core/Models/BucketVersion.cs index 694e360..bf17921 100644 --- a/OpenBudgeteer.Core/Models/BucketVersion.cs +++ b/OpenBudgeteer.Core/Models/BucketVersion.cs @@ -87,7 +87,7 @@ public int BucketTypeXParam private decimal _bucketTypeYParam; /// - /// Parameter for a Amount values. For BucketType: + /// Parameter for an Amount value. For BucketType: /// /// 1 - 0
/// 2-3 - decimal Amount
@@ -103,7 +103,7 @@ public decimal BucketTypeYParam private DateTime _bucketTypeZParam; /// - /// Parameter for date values. For BucketType: + /// Parameter for target date value. For BucketType: /// /// 1-2 - string.Empty
/// 3 - DateTime First target date
diff --git a/OpenBudgeteer.Core/OpenBudgeteer.Core.csproj b/OpenBudgeteer.Core/OpenBudgeteer.Core.csproj new file mode 100644 index 0000000..47678e3 --- /dev/null +++ b/OpenBudgeteer.Core/OpenBudgeteer.Core.csproj @@ -0,0 +1,51 @@ + + + + netcoreapp3.1 + + + + + + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + + + + + + + + 20200605093534_InitialCreate.cs + + + 20200608152707_DecimalPrecision.cs + + + 20200612082229_FixedBucketVersion.cs + + + 20200622121904_DefaultIncomeTransferBucket.cs + + + 20200701071320_BucketColor.cs + + + 20200701133458_DateTimeDataType.cs + + + 20200704081215_ImportProfileDelimiter.cs + + + 20200707141613_ImportProfileDateNumberFormat.cs + + + 20200723111131_BucketNotes.cs + + + 20200822152402_AutomaticBucketAssignment.cs + + + + diff --git a/OpenBudgeteer.Core/OpenBudgeteer.Core.projitems b/OpenBudgeteer.Core/OpenBudgeteer.Core.projitems deleted file mode 100644 index 87ac238..0000000 --- a/OpenBudgeteer.Core/OpenBudgeteer.Core.projitems +++ /dev/null @@ -1,45 +0,0 @@ - - - - $(MSBuildAllProjects);$(MSBuildThisFileFullPath) - true - cc22a1e2-fa88-417c-a402-69526c33d2f4 - - - OpenBudgeteer.Core - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - \ No newline at end of file diff --git a/OpenBudgeteer.Core/OpenBudgeteer.Core.shproj b/OpenBudgeteer.Core/OpenBudgeteer.Core.shproj deleted file mode 100644 index 6ae6599..0000000 --- a/OpenBudgeteer.Core/OpenBudgeteer.Core.shproj +++ /dev/null @@ -1,13 +0,0 @@ - - - - cc22a1e2-fa88-417c-a402-69526c33d2f4 - 14.0 - - - - - - - - diff --git a/OpenBudgeteer.Core/ViewModels/AccountViewModel.cs b/OpenBudgeteer.Core/ViewModels/AccountViewModel.cs index 408834f..9704d65 100644 --- a/OpenBudgeteer.Core/ViewModels/AccountViewModel.cs +++ b/OpenBudgeteer.Core/ViewModels/AccountViewModel.cs @@ -6,7 +6,7 @@ using System.Text; using System.Windows; using Microsoft.EntityFrameworkCore; -using OpenBudgeteer.Core.Common; +using OpenBudgeteer.Core.Common.Database; using OpenBudgeteer.Core.Common.EventClasses; using OpenBudgeteer.Core.Models; using OpenBudgeteer.Core.ViewModels.ItemViewModels; @@ -16,29 +16,45 @@ namespace OpenBudgeteer.Core.ViewModels public class AccountViewModel : ViewModelBase { private ObservableCollection _accounts; + /// + /// Collection of ViewModelItems for Model + /// public ObservableCollection Accounts { get => _accounts; set => Set(ref _accounts, value); } + /// + /// EventHandler which should be invoked in case the whole ViewModel has to be reloaded + /// e.g. due to various database record changes + /// public event EventHandler ViewModelReloadRequired; private readonly DbContextOptions _dbOptions; + /// + /// Basic constructor + /// + /// Options to connect to a database public AccountViewModel(DbContextOptions dbOptions) { _dbOptions = dbOptions; Accounts = new ObservableCollection(); } + /// + /// Initialize ViewModel and load data from database + /// public void LoadData() { Accounts.Clear(); using (var accountDbContext = new DatabaseContext(_dbOptions)) { - foreach (var account in accountDbContext.Account.Where(i => i.IsActive == 1).OrderBy(i => i.Name)) + foreach (var account in accountDbContext.Account + .Where(i => i.IsActive == 1) + .OrderBy(i => i.Name)) { var newAccountItem = new AccountViewModelItem(_dbOptions, account); decimal newIn = 0; @@ -69,6 +85,10 @@ public void LoadData() } } + /// + /// Creates an initial which can be used for further manipulation + /// + /// Newly initialized public AccountViewModelItem PrepareNewAccount() { var result = new AccountViewModelItem(_dbOptions) @@ -83,6 +103,9 @@ public AccountViewModelItem PrepareNewAccount() return result; } + /// + /// Forces reload of ViewModel to revoke unsaved changes + /// public void CancelEditMode() { ViewModelReloadRequired?.Invoke(this, new ViewModelReloadEventArgs(this)); diff --git a/OpenBudgeteer.Core/ViewModels/BucketViewModel.cs b/OpenBudgeteer.Core/ViewModels/BucketViewModel.cs index ae99a03..041cc1a 100644 --- a/OpenBudgeteer.Core/ViewModels/BucketViewModel.cs +++ b/OpenBudgeteer.Core/ViewModels/BucketViewModel.cs @@ -6,12 +6,13 @@ using System.Text; using System.Text.RegularExpressions; using System.Windows; -using OpenBudgeteer.Core.Common; +using OpenBudgeteer.Core.Common.Database; using OpenBudgeteer.Core.Models; using OpenBudgeteer.Core.ViewModels.ItemViewModels; using Microsoft.EntityFrameworkCore; using System.Threading.Tasks; using Microsoft.EntityFrameworkCore.Internal; +using OpenBudgeteer.Core.Common; using OpenBudgeteer.Core.Common.EventClasses; namespace OpenBudgeteer.Core.ViewModels @@ -19,75 +20,99 @@ namespace OpenBudgeteer.Core.ViewModels public class BucketViewModel : ViewModelBase { private decimal _income; + /// + /// Money that has been added to a Bucket + /// public decimal Income { get => _income; - set => Set(ref _income, value); + private set => Set(ref _income, value); } private decimal _expenses; + /// + /// Money that has been moved out of the Bucket + /// public decimal Expenses { get => _expenses; - set => Set(ref _expenses, value); + private set => Set(ref _expenses, value); } private decimal _monthBalance; + /// + /// Combined Income and Expenses in a specific month + /// public decimal MonthBalance { get => _monthBalance; - set => Set(ref _monthBalance, value); + private set => Set(ref _monthBalance, value); } private decimal _budget; + /// + /// Available Money in a specific month + /// public decimal Budget { get => _budget; - set => Set(ref _budget, value); + private set => Set(ref _budget, value); } private decimal _bankBalance; + /// + /// Money available on all bank accounts + /// public decimal BankBalance { get => _bankBalance; - set => Set(ref _bankBalance, value); + private set => Set(ref _bankBalance, value); } private decimal _pendingWant; + /// + /// Money expected to be added to a Bucket in a specific month + /// public decimal PendingWant { get => _pendingWant; - set => Set(ref _pendingWant, value); + private set => Set(ref _pendingWant, value); } private decimal _remainingBudget; + /// + /// Remaining Money in a specific month. Includes Want and negative Balances + /// public decimal RemainingBudget { get => _remainingBudget; - set => Set(ref _remainingBudget, value); + private set => Set(ref _remainingBudget, value); } private decimal _negativeBucketBalance; + /// + /// Sum of all Bucket Balances where the number is negative + /// public decimal NegativeBucketBalance { get => _negativeBucketBalance; - set => Set(ref _negativeBucketBalance, value); + private set => Set(ref _negativeBucketBalance, value); } private ObservableCollection _bucketGroups; + /// + /// Collection of Groups which contains a set of Buckets + /// public ObservableCollection BucketGroups { get => _bucketGroups; - set => Set(ref _bucketGroups, value); - } - - private ObservableCollection _months; - public ObservableCollection Months - { - get => _months; - set => Set(ref _months, value); + private set => Set(ref _bucketGroups, value); } + /// + /// EventHandler which should be invoked in case the whole ViewModel has to be reloaded + /// e.g. due to various database record changes + /// public event EventHandler ViewModelReloadRequired; private readonly DbContextOptions _dbOptions; @@ -95,6 +120,11 @@ public ObservableCollection Months private bool _defaultCollapseState; // Keep Collapse State e.g. after YearMonth change of ViewModel reload + /// + /// Basic constructor + /// + /// Options to connect to a database + /// ViewModel instance to handle selection of a year and month public BucketViewModel(DbContextOptions dbOptions, YearMonthSelectorViewModel yearMonthViewModel) { _dbOptions = dbOptions; @@ -103,7 +133,11 @@ public BucketViewModel(DbContextOptions dbOptions, YearMonthSel //_yearMonthViewModel.SelectedYearMonthChanged += (sender) => { LoadData(); }; } - public async Task> LoadDataAsync() + /// + /// Initialize ViewModel and load data from database + /// + /// Object which contains information and results of this method + public async Task LoadDataAsync() { try { @@ -147,19 +181,23 @@ public async Task> LoadDataAsync() BucketGroups.Add(newBucketGroup); } } - var (success, errorMessage) = UpdateBalanceFigures(); - if (!success) throw new Exception(errorMessage); + var result = UpdateBalanceFigures(); + if (!result.IsSuccessful) throw new Exception(result.Message); } catch (Exception e) { - return new Tuple(false, $"Error during loading: {e.Message}"); + return new ViewModelOperationResult(false, $"Error during loading: {e.Message}"); } - return new Tuple(true, string.Empty); + return new ViewModelOperationResult(true); } - public Tuple CreateGroup() + /// + /// Creates an initial and adds it to ViewModel and Database. + /// Will be added on first position and updates all other Positions accordingly + /// + /// Object which contains information and results of this method + public ViewModelOperationResult CreateGroup() { - var newPosition = BucketGroups.Count == 0 ? 1 : BucketGroups.Last().BucketGroup.Position + 1; var newGroup = new BucketGroup { BucketGroupId = 0, @@ -173,8 +211,7 @@ public Tuple CreateGroup() bucketGroup.BucketGroup.Position++; dbContext.UpdateBucketGroup(bucketGroup.BucketGroup); } - if (dbContext.CreateBucketGroup(newGroup) == 0) - return new Tuple(false, "Unable to write changes to database"); + if (dbContext.CreateBucketGroup(newGroup) == 0) return new ViewModelOperationResult(false, "Unable to write changes to database"); } var newBucketGroupViewModelItem = @@ -188,10 +225,17 @@ public Tuple CreateGroup() ViewModelReloadRequired?.Invoke(this, new ViewModelReloadEventArgs(args.ViewModel)); }; BucketGroups.Insert(0, newBucketGroupViewModelItem); - return new Tuple(true, string.Empty); + return new ViewModelOperationResult(true); } - public Tuple DeleteGroup(BucketGroupViewModelItem bucketGroup) + /// + /// Starts deletion process in the passed and updates positions of + /// all other accordingly + /// + /// Triggers + /// Instance that needs to be deleted + /// Object which contains information and results of this method + public ViewModelOperationResult DeleteGroup(BucketGroupViewModelItem bucketGroup) { var index = BucketGroups.IndexOf(bucketGroup) + 1; var bucketGroupsToMove = BucketGroups.ToList().GetRange(index, BucketGroups.Count - index); @@ -202,9 +246,9 @@ public Tuple DeleteGroup(BucketGroupViewModelItem bucketGroup) { try { - var result = bucketGroup.DeleteGroup(); - if (!result.Item1) throw new Exception(result.Item2); - + if (bucketGroup.Buckets.Count > 0) throw new Exception("Groups with Buckets cannot be deleted."); + dbContext.DeleteBucketGroup(bucketGroup.BucketGroup); + var dbBucketGroups = new List(); foreach (var bucketGroupViewModelItem in bucketGroupsToMove) { @@ -213,21 +257,27 @@ public Tuple DeleteGroup(BucketGroupViewModelItem bucketGroup) } dbContext.UpdateBucketGroups(dbBucketGroups); + transaction.Commit(); + ViewModelReloadRequired?.Invoke(this, new ViewModelReloadEventArgs(this)); + return new ViewModelOperationResult(true, true); } catch (Exception e) { transaction.Rollback(); - return new Tuple(false, e.Message); + return new ViewModelOperationResult(false, e.Message); } } } - - ViewModelReloadRequired?.Invoke(this, new ViewModelReloadEventArgs(this)); - return new Tuple(true, string.Empty); } - public Tuple DistributeBudget() + /// + /// Put money into all Buckets according to their Want. Saves the results to the database. + /// + /// Doesn't consider any available Budget figures. + /// Triggers + /// Object which contains information and results of this method + public ViewModelOperationResult DistributeBudget() { using (var dbContext = new DatabaseContext(_dbOptions)) { @@ -244,25 +294,29 @@ public Tuple DistributeBudget() { if (bucket.Want == 0) continue; bucket.InOut = bucket.Want; - var (success, errorMessage) = bucket.HandleInOutInput("Enter"); - if (!success) throw new Exception(errorMessage); + var result = bucket.HandleInOutInput("Enter"); + if (!result.IsSuccessful) throw new Exception(result.Message); } + transaction.Commit(); + //UpdateBalanceFigures(); // Should be done but not required because it will be done during ViewModel reload + ViewModelReloadRequired?.Invoke(this, new ViewModelReloadEventArgs(this)); + return new ViewModelOperationResult(true, true); } catch (Exception e) { transaction.Rollback(); - return new Tuple(false, $"Error during Budget distribution: {e.Message}"); + return new ViewModelOperationResult(false, $"Error during Budget distribution: {e.Message}"); } - } } - //UpdateBalanceFigures(); // Should be done but not required because it will be done during ViewModel reload - ViewModelReloadRequired?.Invoke(this, new ViewModelReloadEventArgs(this)); - return new Tuple(true, string.Empty); } - public Tuple UpdateBalanceFigures() + /// + /// Re-calculates figures of the ViewModel like Budget and Balances + /// + /// Object which contains information and results of this method + public ViewModelOperationResult UpdateBalanceFigures() { try { @@ -313,12 +367,16 @@ public Tuple UpdateBalanceFigures() } catch (Exception e) { - return new Tuple(false, $"Error during Balance recalculation: {e.Message}"); + return new ViewModelOperationResult(false, $"Error during Balance recalculation: {e.Message}"); } - return new Tuple(true, string.Empty); + return new ViewModelOperationResult(true); } + /// + /// Helper methode to set Collapse status for all + /// + /// New collapse status public void ChangeBucketGroupCollapse(bool collapse = true) { _defaultCollapseState = collapse; @@ -328,17 +386,30 @@ public void ChangeBucketGroupCollapse(bool collapse = true) } } - public Tuple SaveChanges(BucketViewModelItem bucket) + /// + /// Helper method to start Save process for the passed + /// + /// Triggers also update of ViewModel figures + /// instance with modifications + /// Object which contains information and results of this method + public ViewModelOperationResult SaveChanges(BucketViewModelItem bucket) { - var result = bucket.SaveChanges(); - if (!result.Item1) return result; + var result = bucket.CreateOrUpdateBucket(); + if (!result.IsSuccessful) return result; return UpdateBalanceFigures(); } - public Tuple CloseBucket(BucketViewModelItem bucket) + /// + /// Helper method to start Deletion process for the passed + /// + /// Triggers also update of ViewModel figures + /// Triggers + /// instance with containing to be closed + /// Object which contains information and results of this method + public ViewModelOperationResult CloseBucket(BucketViewModelItem bucket) { var result = bucket.CloseBucket(); - if (!result.Item1) return result; + if (!result.IsSuccessful) return result; return UpdateBalanceFigures(); } } diff --git a/OpenBudgeteer.Core/ViewModels/ImportDataViewModel.cs b/OpenBudgeteer.Core/ViewModels/ImportDataViewModel.cs index bc165af..74886a7 100644 --- a/OpenBudgeteer.Core/ViewModels/ImportDataViewModel.cs +++ b/OpenBudgeteer.Core/ViewModels/ImportDataViewModel.cs @@ -1,5 +1,5 @@ using Microsoft.EntityFrameworkCore; -using OpenBudgeteer.Core.Common; +using OpenBudgeteer.Core.Common.Database; using OpenBudgeteer.Core.Models; using System; using System.Collections.Generic; @@ -10,6 +10,7 @@ using System.Linq; using System.Text; using System.Threading.Tasks; +using OpenBudgeteer.Core.Common; using TinyCsvParser; using TinyCsvParser.Mapping; using TinyCsvParser.Tokenizer.RFC4180; @@ -21,6 +22,15 @@ public class ImportDataViewModel : ViewModelBase { private class CsvBankTransactionMapping : CsvMapping { + /// + /// Definition on how CSV columns should be mapped to + /// + /// + /// instance and collection of columns will be used to identify columnIndex for + /// CSV mapping + /// + /// Instance required for CSV column name + /// Collection of all CSV columns public CsvBankTransactionMapping(ImportProfile importProfile, IEnumerable identifiedColumns) : base() { // TODO Add User Input for CultureInfo for Amount & TransactionDate conversion @@ -36,6 +46,9 @@ public CsvBankTransactionMapping(ImportProfile importProfile, IEnumerable + /// Path to the file which should be imported + ///
public string FilePath { get => _filePath; @@ -43,6 +56,9 @@ public string FilePath } private string _fileText; + /// + /// Readonly content of the file + /// public string FileText { get => _fileText; @@ -50,6 +66,9 @@ public string FileText } private Account _selectedAccount; + /// + /// Target for which all imported should be added + /// public Account SelectedAccount { get => _selectedAccount; @@ -57,6 +76,9 @@ public Account SelectedAccount } private ImportProfile _selectedImportProfile; + /// + /// Selected profile with import settings from the database + /// public ImportProfile SelectedImportProfile { get => _selectedImportProfile; @@ -64,72 +86,83 @@ public ImportProfile SelectedImportProfile } private int _totalRecords; + /// + /// Number of records identified in the file + /// public int TotalRecords { get => _totalRecords; - set => Set(ref _totalRecords, value); + private set => Set(ref _totalRecords, value); } private int _recordsWithErrors; + /// + /// Number of records where import will fail or has failed + /// public int RecordsWithErrors { get => _recordsWithErrors; - set => Set(ref _recordsWithErrors, value); + private set => Set(ref _recordsWithErrors, value); } private int _validRecords; + /// + /// Number of records where import will be or was successful + /// public int ValidRecords { get => _validRecords; - set => Set(ref _validRecords, value); - } - - private bool _isModificationEnabled; - public bool IsModificationEnabled - { - get => _isModificationEnabled; - set => Set(ref _isModificationEnabled, value); - } - - private bool _isColumnMappingSettingVisible; - public bool IsColumnMappingSettingVisible - { - get => _isColumnMappingSettingVisible; - set => Set(ref _isColumnMappingSettingVisible, value); + private set => Set(ref _validRecords, value); } private ObservableCollection _availableImportProfiles; + /// + /// Available in the database + /// public ObservableCollection AvailableImportProfiles { get => _availableImportProfiles; - set => Set(ref _availableImportProfiles, value); + private set => Set(ref _availableImportProfiles, value); } private ObservableCollection _availableAccounts; + /// + /// Helper collection to list all available in the database + /// public ObservableCollection AvailableAccounts { get => _availableAccounts; - set => Set(ref _availableAccounts, value); + private set => Set(ref _availableAccounts, value); } private ObservableCollection _identifiedColumns; + /// + /// Collection of columns that have been identified in the CSV file + /// public ObservableCollection IdentifiedColumns { get => _identifiedColumns; - set => Set(ref _identifiedColumns, value); + private set => Set(ref _identifiedColumns, value); } private ObservableCollection> _parsedRecords; + /// + /// Results of + /// public ObservableCollection> ParsedRecords { get => _parsedRecords; - set => Set(ref _parsedRecords, value); + private set => Set(ref _parsedRecords, value); } private bool _isProfileValid; private string[] _fileLines; private readonly DbContextOptions _dbOptions; + /// + /// Basic constructor + /// + /// Options to connect to a database public ImportDataViewModel(DbContextOptions dbOptions) { AvailableImportProfiles = new ObservableCollection(); @@ -141,7 +174,11 @@ public ImportDataViewModel(DbContextOptions dbOptions) _dbOptions = dbOptions; } - public Tuple LoadData() + /// + /// Initialize ViewModel and load data from database + /// + /// + public ViewModelOperationResult LoadData() { try { @@ -153,34 +190,58 @@ public Tuple LoadData() AvailableAccounts.Add(account); } } + return new ViewModelOperationResult(true); } catch (Exception e) { - return new Tuple(false, $"Error during loading: {e.Message}"); + return new ViewModelOperationResult(false, $"Error during loading: {e.Message}"); } - return new Tuple(true, string.Empty); } - public Tuple HandleOpenFile(string[] dialogResults) + /// + /// Open a file based on and read its content + /// + /// Object which contains information and results of this method + public ViewModelOperationResult HandleOpenFile() { try { - if (!dialogResults.Any()) return new Tuple(true, string.Empty); - FilePath = dialogResults[0]; FileText = File.ReadAllText(FilePath, Encoding.GetEncoding(1252)); _fileLines = File.ReadAllLines(FilePath, Encoding.GetEncoding(1252)); - IsModificationEnabled = true; + return new ViewModelOperationResult(true); } catch (Exception e) { - return new Tuple(false, $"Error during loading: {e.Message}"); + return new ViewModelOperationResult(false, $"Error during loading: {e.Message}"); } - return new Tuple(true, string.Empty); - } - public async Task> HandleOpenFileAsync(Stream stream) + /// + /// Open a file based on results of an OpenFileDialog and read its content + /// + /// OpenFileDialog results + /// Object which contains information and results of this method + public ViewModelOperationResult HandleOpenFile(string[] dialogResults) + { + try + { + if (!dialogResults.Any()) return new ViewModelOperationResult(true); + FilePath = dialogResults[0]; + return HandleOpenFile(); + } + catch (Exception e) + { + return new ViewModelOperationResult(false, $"Error during loading: {e.Message}"); + } + } + + /// + /// Open a file based on a and read its content + /// + /// Stream to the file + /// Object which contains information and results of this method + public async Task HandleOpenFileAsync(Stream stream) { try { @@ -200,22 +261,23 @@ public async Task> HandleOpenFileAsync(Stream stream) FileText = stringBuilder.ToString(); _fileLines = newLines.ToArray(); - IsModificationEnabled = true; + return new ViewModelOperationResult(true); } catch (Exception e) { - return new Tuple(false, $"Unable to open file: {e.Message}"); + return new ViewModelOperationResult(false, $"Unable to open file: {e.Message}"); } - return new Tuple(true, string.Empty); - } - public Tuple LoadProfile() + /// + /// Loads all settings based on + /// + /// Object which contains information and results of this method + public ViewModelOperationResult LoadProfile() { try { ResetLoadedProfileData(); - IsColumnMappingSettingVisible = true; // Set target Account if (AvailableAccounts.Any(i => i.AccountId == SelectedImportProfile.AccountId)) @@ -223,22 +285,26 @@ public Tuple LoadProfile() SelectedAccount = AvailableAccounts.First(i => i.AccountId == SelectedImportProfile.AccountId); } - var (success, errorMessage) = LoadHeaders(); - if (!success) throw new Exception(errorMessage); + var result = LoadHeaders(); + if (!result.IsSuccessful) throw new Exception(result.Message); ValidateData(); - _isProfileValid = true; + + return new ViewModelOperationResult(true); } catch (Exception e) { _isProfileValid = false; - return new Tuple(false, $"Unable to load Profile: {e.Message}"); + return new ViewModelOperationResult(false, $"Unable to load Profile: {e.Message}"); } - return new Tuple(true, string.Empty); } - public Tuple LoadHeaders() + /// + /// Reads column headers from the loaded file + /// + /// Object which contains information and results of this method + public ViewModelOperationResult LoadHeaders() { try { @@ -251,25 +317,36 @@ public Tuple LoadHeaders() if (column != string.Empty) IdentifiedColumns.Add(column.Trim(SelectedImportProfile.TextQualifier)); // Exclude TextQualifier } + + return new ViewModelOperationResult(true); } catch (Exception e) { - return new Tuple(false, $"Unable to load Headers: {e.Message}"); + return new ViewModelOperationResult(false, $"Unable to load Headers: {e.Message}"); } - return new Tuple(true, string.Empty); } + /// + /// Reset all figures and parsed records + /// private void ResetLoadedProfileData() { SelectedAccount = new Account(); - IsColumnMappingSettingVisible = false; ParsedRecords.Clear(); TotalRecords = 0; RecordsWithErrors = 0; ValidRecords = 0; } - public string ValidateData() + /// + /// Reads the file and parses the content to a set of . + /// Results will be stored in + /// + /// + /// Sets also figures of the ViewModel like or + /// + /// Object which contains information and results of this method + public ViewModelOperationResult ValidateData() { try { @@ -303,7 +380,7 @@ public string ValidateData() ValidRecords = parsedResults.Count(i => i.IsValid); if (ValidRecords > 0) _isProfileValid = true; - return string.Empty; + return new ViewModelOperationResult(true); } catch (Exception e) { @@ -311,16 +388,21 @@ public string ValidateData() RecordsWithErrors = 0; ValidRecords = 0; ParsedRecords.Clear(); - return e.Message; + return new ViewModelOperationResult(false, e.Message); } } - public Tuple ImportData() + /// + /// Uses data from to store it in the database + /// + /// + /// This method will call + /// + /// Object which contains information and results of this method + public ViewModelOperationResult ImportData() { - if (!_isProfileValid) - return new Tuple(false, "Unable to Import Data as current settings are invalid."); - - var importedCount = 0; + if (!_isProfileValid) return new ViewModelOperationResult(false, "Unable to Import Data as current settings are invalid."); + ValidateData(); using (var dbContext = new DatabaseContext(_dbOptions)) { @@ -328,6 +410,7 @@ public Tuple ImportData() { try { + var importedCount = 0; var newRecords = new List(); foreach (var parsedRecord in ParsedRecords.Where(i => i.IsValid)) { @@ -336,19 +419,22 @@ public Tuple ImportData() newRecords.Add(newRecord); } importedCount = dbContext.CreateBankTransactions(newRecords); + transaction.Commit(); + return new ViewModelOperationResult(true, $"Successfully imported {importedCount} records."); } catch (Exception e) { transaction.Rollback(); - return new Tuple(false, $"Unable to Import Data. Error message: {e.Message}"); + return new ViewModelOperationResult(false, $"Unable to Import Data. Error message: {e.Message}"); } } } - - return new Tuple(true, $"Successfully imported {importedCount} records."); } + /// + /// Helper method to load from the database + /// private void LoadAvailableProfiles() { AvailableImportProfiles.Clear(); @@ -361,7 +447,11 @@ private void LoadAvailableProfiles() } } - public Tuple CreateProfile() + /// + /// Creates a new in the database based on data + /// + /// Object which contains information and results of this method + public ViewModelOperationResult CreateProfile() { try { @@ -374,16 +464,21 @@ public Tuple CreateProfile() throw new Exception("Profile could not be created in database."); LoadAvailableProfiles(); SelectedImportProfile = AvailableImportProfiles.First(i => i.ImportProfileId == SelectedImportProfile.ImportProfileId); - } + } + + return new ViewModelOperationResult(true); } catch (Exception e) { - return new Tuple(false, $"Unable to create Import Profile: {e.Message}"); + return new ViewModelOperationResult(false, $"Unable to create Import Profile: {e.Message}"); } - return new Tuple(true, string.Empty); } - public Tuple SaveProfile() + /// + /// Updates data of the current in the database + /// + /// Object which contains information and results of this method + public ViewModelOperationResult SaveProfile() { try { @@ -395,16 +490,21 @@ public Tuple SaveProfile() LoadAvailableProfiles(); SelectedImportProfile = AvailableImportProfiles.First(i => i.ImportProfileId == SelectedImportProfile.ImportProfileId); - } + } + + return new ViewModelOperationResult(true); } catch (Exception e) { - return new Tuple(false, $"Unable to save Import Profile: {e.Message}"); + return new ViewModelOperationResult(false, $"Unable to save Import Profile: {e.Message}"); } - return new Tuple(true, string.Empty); } - public Tuple DeleteProfile() + /// + /// Deletes the in the database based on + /// + /// Object which contains information and results of this method + public ViewModelOperationResult DeleteProfile() { try { @@ -414,12 +514,13 @@ public Tuple DeleteProfile() } ResetLoadedProfileData(); LoadAvailableProfiles(); + + return new ViewModelOperationResult(true); } catch (Exception e) { - return new Tuple(false, $"Unable to delete Import Profile: {e.Message}"); + return new ViewModelOperationResult(false, $"Unable to delete Import Profile: {e.Message}"); } - return new Tuple(true, string.Empty); } } } diff --git a/OpenBudgeteer.Core/ViewModels/ItemViewModels/AccountViewModelItem.cs b/OpenBudgeteer.Core/ViewModels/ItemViewModels/AccountViewModelItem.cs index 2d54923..a22de03 100644 --- a/OpenBudgeteer.Core/ViewModels/ItemViewModels/AccountViewModelItem.cs +++ b/OpenBudgeteer.Core/ViewModels/ItemViewModels/AccountViewModelItem.cs @@ -3,6 +3,7 @@ using System.Text; using Microsoft.EntityFrameworkCore; using OpenBudgeteer.Core.Common; +using OpenBudgeteer.Core.Common.Database; using OpenBudgeteer.Core.Common.EventClasses; using OpenBudgeteer.Core.Models; @@ -11,6 +12,9 @@ namespace OpenBudgeteer.Core.ViewModels.ItemViewModels public class AccountViewModelItem : ViewModelBase { private Account _account; + /// + /// Reference to model object in the database + /// public Account Account { get => _account; @@ -18,6 +22,9 @@ public Account Account } private decimal _balance; + /// + /// Total account balance + /// public decimal Balance { get => _balance; @@ -25,6 +32,9 @@ public decimal Balance } private decimal _in; + /// + /// Total income of the account + /// public decimal In { get => _in; @@ -32,61 +42,74 @@ public decimal In } private decimal _out; + /// + /// Total expenses of the account + /// public decimal Out { get => _out; set => Set(ref _out, value); } - private bool _inModification; - public bool InModification - { - get => _inModification; - set => Set(ref _inModification, value); - } - + /// + /// EventHandler which should be invoked in case the whole ViewModel has to be reloaded + /// e.g. due to various database record changes + /// public event EventHandler ViewModelReloadRequired; private readonly DbContextOptions _dbOptions; + /// + /// Basic constructor + /// + /// Options to connect to a database public AccountViewModelItem(DbContextOptions dbOptions) { _dbOptions = dbOptions; } + /// + /// Initialize ViewModel with an existing object + /// + /// Options to connect to a database + /// Account instance public AccountViewModelItem(DbContextOptions dbOptions, Account account) : this(dbOptions) { _account = account; } - public Tuple CreateUpdateAccount() + /// + /// Creates or updates a record in the database based on object + /// + /// Triggers + /// Object which contains information and results of this method + public ViewModelOperationResult CreateUpdateAccount() { using (var dbContext = new DatabaseContext(_dbOptions)) { var result = Account.AccountId == 0 ? dbContext.CreateAccount(Account) : dbContext.UpdateAccount(Account); - if (result == 0) - { - return new Tuple(false, "Unable to save changes to database"); - } + if (result == 0) return new ViewModelOperationResult(false, "Unable to save changes to database"); ViewModelReloadRequired?.Invoke(this, new ViewModelReloadEventArgs(this)); - return new Tuple(true, string.Empty); + return new ViewModelOperationResult(true, true); } } - public Tuple CloseAccount() + /// + /// Sets Inactive flag for a record in the database based on object. + /// + /// Triggers + /// Object which contains information and results of this method + public ViewModelOperationResult CloseAccount() { - if (Balance != 0) return new Tuple(false, "Balance must be 0 to close an Account"); + if (Balance != 0) return new ViewModelOperationResult(false, "Balance must be 0 to close an Account"); Account.IsActive = 0; using (var dbContext = new DatabaseContext(_dbOptions)) { var result = dbContext.UpdateAccount(Account); - if (result == 0) - { - return new Tuple(false, "Unable to save changes to database"); - } + if (result == 0) return new ViewModelOperationResult(false, "Unable to save changes to database"); ViewModelReloadRequired?.Invoke(this, new ViewModelReloadEventArgs(this)); - return new Tuple(true, string.Empty); + return new ViewModelOperationResult(true, true); } } } diff --git a/OpenBudgeteer.Core/ViewModels/ItemViewModels/BucketGroupViewModelItem.cs b/OpenBudgeteer.Core/ViewModels/ItemViewModels/BucketGroupViewModelItem.cs index 9077e63..d09db17 100644 --- a/OpenBudgeteer.Core/ViewModels/ItemViewModels/BucketGroupViewModelItem.cs +++ b/OpenBudgeteer.Core/ViewModels/ItemViewModels/BucketGroupViewModelItem.cs @@ -1,4 +1,4 @@ -using OpenBudgeteer.Core.Common; +using OpenBudgeteer.Core.Common.Database; using OpenBudgeteer.Core.Models; using System; using System.Collections.Generic; @@ -6,6 +6,7 @@ using System.Linq; using System.Text; using Microsoft.EntityFrameworkCore; +using OpenBudgeteer.Core.Common; using OpenBudgeteer.Core.Common.EventClasses; namespace OpenBudgeteer.Core.ViewModels.ItemViewModels @@ -13,6 +14,9 @@ namespace OpenBudgeteer.Core.ViewModels.ItemViewModels public class BucketGroupViewModelItem : ViewModelBase { private BucketGroup _bucketGroup; + /// + /// Reference to model object in the database + /// public BucketGroup BucketGroup { get => _bucketGroup; @@ -20,6 +24,9 @@ public BucketGroup BucketGroup } private decimal _totalBalance; + /// + /// Balance of all Buckets assigned to the BucketGroup + /// public decimal TotalBalance { get => _totalBalance; @@ -27,6 +34,9 @@ public decimal TotalBalance } private bool _isHovered; + /// + /// Helper property to check if the cursor hovers over the entry in the UI + /// public bool IsHovered { get => _isHovered; @@ -34,6 +44,9 @@ public bool IsHovered } private bool _isCollapsed; + /// + /// Helper property to check if the list of assigned Buckets is collapsed + /// public bool IsCollapsed { get => _isCollapsed; @@ -41,6 +54,9 @@ public bool IsCollapsed } private ObservableCollection _buckets; + /// + /// Collection of Buckets assigned to this BucketGroup + /// public ObservableCollection Buckets { get => _buckets; @@ -48,18 +64,29 @@ public ObservableCollection Buckets } private bool _inModification; + /// + /// Helper property to check if the BucketGroup is currently modified + /// public bool InModification { get => _inModification; set => Set(ref _inModification, value); } + /// + /// EventHandler which should be invoked in case the whole ViewModel has to be reloaded + /// e.g. due to various database record changes + /// public event EventHandler ViewModelReloadRequired; - internal DateTime CurrentMonth; + private readonly DateTime _currentMonth; private readonly DbContextOptions _dbOptions; private BucketGroup _oldBucketGroup; + /// + /// Basic constructor + /// + /// Options to connect to a database public BucketGroupViewModelItem(DbContextOptions dbOptions) { Buckets = new ObservableCollection(); @@ -67,12 +94,21 @@ public BucketGroupViewModelItem(DbContextOptions dbOptions) _dbOptions = dbOptions; } + /// + /// Initialize ViewModel based on an existing object and a specific YearMonth + /// + /// Options to connect to a database + /// BucketGroup instance + /// YearMonth that should be used public BucketGroupViewModelItem(DbContextOptions dbOptions, BucketGroup bucketGroup, DateTime currentMonth) : this(dbOptions) { BucketGroup = bucketGroup; - CurrentMonth = currentMonth; + _currentMonth = currentMonth; } + /// + /// Helper method to start modification process and creating a backup of current values + /// public void StartModification() { _oldBucketGroup = new BucketGroup() @@ -84,6 +120,9 @@ public void StartModification() InModification = true; } + /// + /// Stops modification and restores previous values + /// public void CancelModification() { BucketGroup = _oldBucketGroup; @@ -91,7 +130,12 @@ public void CancelModification() _oldBucketGroup = null; } - public Tuple SaveModification() + /// + /// Updates a record in the database based on object + /// + /// Triggers + /// Object which contains information and results of this method + public ViewModelOperationResult SaveModification() { try { @@ -99,38 +143,27 @@ public Tuple SaveModification() { dbContext.UpdateBucketGroup(BucketGroup); } - ViewModelReloadRequired?.Invoke(this, new ViewModelReloadEventArgs(this)); InModification = false; _oldBucketGroup = null; + ViewModelReloadRequired?.Invoke(this, new ViewModelReloadEventArgs(this)); + return new ViewModelOperationResult(true, true); } catch (Exception e) { - return new Tuple(false, $"Unable to write changes to database: {e.Message}"); - } - return new Tuple(true, string.Empty); - } - - public Tuple DeleteGroup() - { - try - { - if (Buckets.Count > 0) throw new Exception("Groups with Buckets cannot be deleted."); - - using (var dbContext = new DatabaseContext(_dbOptions)) - { - dbContext.DeleteBucketGroup(BucketGroup); - } - } - catch (Exception e) - { - return new Tuple(false, $"Unable to delete Bucket Group: {e.Message}"); + return new ViewModelOperationResult(false, $"Unable to write changes to database: {e.Message}"); } - return new Tuple(true, string.Empty); } - public Tuple MoveGroup(int positions) + /// + /// Moves the position of the BucketGroup according to the passed value. Updates positions for all other + /// BucketGroups accordingly + /// + /// Number of positions that BucketGroup needs to be moved + /// Triggers + /// Object which contains information and results of this method + public ViewModelOperationResult MoveGroup(int positions) { - if (positions == 0) return new Tuple(true, string.Empty); + if (positions == 0) return new ViewModelOperationResult(true); using (var dbContext = new DatabaseContext(_dbOptions)) { using (var transaction = dbContext.Database.BeginTransaction()) @@ -141,7 +174,7 @@ public Tuple MoveGroup(int positions) var targetPosition = BucketGroup.Position + positions; if (targetPosition < 1) targetPosition = 1; if (targetPosition > bucketGroupCount) targetPosition = bucketGroupCount; - if (targetPosition == BucketGroup.Position) return new Tuple(true, string.Empty); // Group is already at the end or top. No further action + if (targetPosition == BucketGroup.Position) return new ViewModelOperationResult(true); // Group is already at the end or top. No further action // Move Group in an interim List var existingBucketGroups = new ObservableCollection(); foreach (var bucketGroup in dbContext.BucketGroup.OrderBy(i => i.Position)) @@ -161,21 +194,20 @@ public Tuple MoveGroup(int positions) transaction.Commit(); ViewModelReloadRequired?.Invoke(this, new ViewModelReloadEventArgs(this)); + return new ViewModelOperationResult(true, true); } catch (Exception e) { transaction.Rollback(); - return new Tuple(false, $"Unable to move Bucket Group: {e.Message}"); + return new ViewModelOperationResult(false, $"Unable to move Bucket Group: {e.Message}"); } } } - - return new Tuple(true, string.Empty); } public BucketViewModelItem CreateBucket() { - var newBucket = new BucketViewModelItem(_dbOptions, BucketGroup, CurrentMonth); + var newBucket = new BucketViewModelItem(_dbOptions, BucketGroup, _currentMonth); // Hand over ViewModel changes newBucket.ViewModelReloadRequired += (sender, args) => ViewModelReloadRequired?.Invoke(this, new ViewModelReloadEventArgs(this)); diff --git a/OpenBudgeteer.Core/ViewModels/ItemViewModels/BucketViewModelItem.cs b/OpenBudgeteer.Core/ViewModels/ItemViewModels/BucketViewModelItem.cs index 2b0707a..940b06a 100644 --- a/OpenBudgeteer.Core/ViewModels/ItemViewModels/BucketViewModelItem.cs +++ b/OpenBudgeteer.Core/ViewModels/ItemViewModels/BucketViewModelItem.cs @@ -1,4 +1,4 @@ -using OpenBudgeteer.Core.Common; +using OpenBudgeteer.Core.Common.Database; using OpenBudgeteer.Core.Models; using System; using System.Collections.Generic; @@ -9,6 +9,7 @@ using System.Text; using Microsoft.EntityFrameworkCore; using System.Threading.Tasks; +using OpenBudgeteer.Core.Common; using OpenBudgeteer.Core.Common.EventClasses; namespace OpenBudgeteer.Core.ViewModels.ItemViewModels @@ -16,6 +17,9 @@ namespace OpenBudgeteer.Core.ViewModels.ItemViewModels public class BucketViewModelItem : ViewModelBase { private Bucket _bucket; + /// + /// Reference to model object in the database + /// public Bucket Bucket { get => _bucket; @@ -23,6 +27,9 @@ public Bucket Bucket } private BucketVersion _bucketVersion; + /// + /// Reference to model object in the database + /// public BucketVersion BucketVersion { get => _bucketVersion; @@ -101,7 +108,7 @@ public int Progress private bool _isProgressBarVisible; /// - /// Sets the visibility of the ProgressBar if 3 or 4 + /// Helper property to set the visibility of the ProgressBar if 3 or 4 /// public bool IsProgressbarVisible { @@ -110,6 +117,9 @@ public bool IsProgressbarVisible } private bool _isHovered; + /// + /// Helper property to check if the cursor hovers over the entry in the UI + /// public bool IsHovered { get => _isHovered; @@ -117,6 +127,9 @@ public bool IsHovered } private bool _inModification; + /// + /// Helper property to check if the Bucket is currently modified + /// public bool InModification { get => _inModification; @@ -124,6 +137,9 @@ public bool InModification } private ObservableCollection _availableBucketTypes; + /// + /// Helper collection to list BucketTypes explanations + /// public ObservableCollection AvailableBucketTypes { get => _availableBucketTypes; @@ -131,6 +147,9 @@ public ObservableCollection AvailableBucketTypes } private ObservableCollection _availableColors; + /// + /// Helper collection to list available System colors + /// public ObservableCollection AvailableColors { get => _availableColors; @@ -138,18 +157,29 @@ public ObservableCollection AvailableColors } private ObservableCollection _availableBucketGroups; + /// + /// Helper collection to list available where this Bucket can be assigned to + /// public ObservableCollection AvailableBucketGroups { get => _availableBucketGroups; set => Set(ref _availableBucketGroups, value); } + /// + /// EventHandler which should be invoked in case the whole ViewModel has to be reloaded + /// e.g. due to various database record changes + /// public event EventHandler ViewModelReloadRequired; private readonly bool _isNewlyCreatedBucket; private readonly DateTime _currentYearMonth; private readonly DbContextOptions _dbOptions; + /// + /// Basic constructor + /// + /// Options to connect to a database public BucketViewModelItem(DbContextOptions dbOptions) { _dbOptions = dbOptions; @@ -183,6 +213,12 @@ void GetKnownColors() } } + /// + /// Initialize ViewModel based on a specific YearMonth + /// + /// Creates an initial + /// Options to connect to a database + /// YearMonth that should be used public BucketViewModelItem(DbContextOptions dbOptions, DateTime yearMonth) : this(dbOptions) { _currentYearMonth = new DateTime(yearMonth.Year, yearMonth.Month, 1); @@ -195,6 +231,14 @@ public BucketViewModelItem(DbContextOptions dbOptions, DateTime }; } + /// + /// Initialize ViewModel based on an existing object and a specific YearMonth + /// + /// Creates an initial in active modification mode + /// Creates an initial + /// Options to connect to a database + /// BucketGroup instance + /// YearMonth that should be used public BucketViewModelItem(DbContextOptions dbOptions, BucketGroup bucketGroup, DateTime yearMonth) : this(dbOptions, yearMonth) { _isNewlyCreatedBucket = true; @@ -210,17 +254,34 @@ public BucketViewModelItem(DbContextOptions dbOptions, BucketGr }; } + /// + /// Initialize ViewModel based on an existing object and a specific YearMonth + /// + /// Runs to get latest + /// Options to connect to a database + /// Bucket instance + /// YearMonth that should be used public BucketViewModelItem(DbContextOptions dbOptions, Bucket bucket, DateTime yearMonth) : this(dbOptions, yearMonth) { Bucket = bucket; CalculateValues(); } + /// + /// Creates and returns a new ViewModel based on an existing object and a specific YearMonth + /// + /// Options to connect to a database + /// Bucket instance + /// YearMonth that should be used + /// New ViewModel instance public static async Task CreateAsync(DbContextOptions dbOptions, Bucket bucket, DateTime yearMonth) { return await Task.Run(() => new BucketViewModelItem(dbOptions, bucket, yearMonth)); } + /// + /// Identifies latest based on and calculates all figures + /// private void CalculateValues() { Balance = 0; @@ -375,15 +436,26 @@ decimal CalculateWant(DateTime targetDate) #endregion } + /// + /// Activates modification mode + /// public void EditBucket() { InModification = true; } - public Tuple CloseBucket() + /// + /// Updates a record in the database based on object to set it as inactive. In case there + /// are no nor assigned to it, it will be deleted + /// completely from the database (including ) + /// + /// Bucket will be set to inactive for the next month + /// Triggers + /// Object which contains information and results of this method + public ViewModelOperationResult CloseBucket() { - if (Bucket.IsInactive) return new Tuple(false, "Bucket has been already set to inactive"); - if (Balance != 0) return new Tuple(false, "Balance must be 0 to close a Bucket"); + if (Bucket.IsInactive) return new ViewModelOperationResult(false, "Bucket has been already set to inactive"); + if (Balance != 0) return new ViewModelOperationResult(false, "Balance must be 0 to close a Bucket"); using (var dbContext = new DatabaseContext(_dbOptions)) { @@ -427,145 +499,180 @@ public Tuple CloseBucket() catch (Exception e) { transaction.Rollback(); - return new Tuple(false, $"Error during database update: {e.Message}"); + return new ViewModelOperationResult(false, $"Error during database update: {e.Message}"); } } } ViewModelReloadRequired?.Invoke(this, new ViewModelReloadEventArgs(this)); - return new Tuple(true, string.Empty); + return new ViewModelOperationResult(true, true); } - public Tuple SaveChanges() + /// + /// Creates or updates a record in the database based on object + /// + /// Creates also a new record in the database + /// + /// Recalculates figures after database operations in case has not been triggered + /// + /// Can trigger + /// Object which contains information and results of this method + public ViewModelOperationResult CreateOrUpdateBucket() { - var forceViewModelReload = false; + var result = _isNewlyCreatedBucket ? CreateBucket() : UpdateBucket(); + if (!result.IsSuccessful || result.ViewModelReloadInvoked) return result; + InModification = false; + CalculateValues(); + return new ViewModelOperationResult(true); + } - if (_isNewlyCreatedBucket) + /// + /// Creates a new record in the database based on object + /// + /// Creates also a new record in the database + /// Triggers + /// Object which contains information and results of this method + private ViewModelOperationResult CreateBucket() + { + using (var dbContext = new DatabaseContext(_dbOptions)) { - // Create new Bucket - using (var dbContext = new DatabaseContext(_dbOptions)) + using (var transaction = dbContext.Database.BeginTransaction()) { - using (var transaction = dbContext.Database.BeginTransaction()) + try { - try - { - if (dbContext.CreateBucket(Bucket) == 0) - throw new Exception("Unable to create new Bucket."); - - var newBucketVersion = BucketVersion; - newBucketVersion.BucketId = Bucket.BucketId; - newBucketVersion.Version = 1; - newBucketVersion.ValidFrom = _currentYearMonth; - if (dbContext.CreateBucketVersion(newBucketVersion) == 0) - throw new Exception($"Unable to create new Bucket Version.{Environment.NewLine}" + - $"{Environment.NewLine}" + - $"Bucket ID: {newBucketVersion.BucketId}"); + if (dbContext.CreateBucket(Bucket) == 0) + throw new Exception("Unable to create new Bucket."); + + var newBucketVersion = BucketVersion; + newBucketVersion.BucketId = Bucket.BucketId; + newBucketVersion.Version = 1; + newBucketVersion.ValidFrom = _currentYearMonth; + if (dbContext.CreateBucketVersion(newBucketVersion) == 0) + throw new Exception($"Unable to create new Bucket Version.{Environment.NewLine}" + + $"{Environment.NewLine}" + + $"Bucket ID: {newBucketVersion.BucketId}"); - transaction.Commit(); - ViewModelReloadRequired?.Invoke(this, new ViewModelReloadEventArgs(this)); - } - catch (Exception e) - { - transaction.Rollback(); - ViewModelReloadRequired?.Invoke(this, new ViewModelReloadEventArgs(this)); - return new Tuple(false, $"Error during database update: {e.Message}"); - } + transaction.Commit(); + ViewModelReloadRequired?.Invoke(this, new ViewModelReloadEventArgs(this)); + return new ViewModelOperationResult(true, true); } - } - } - else - { - // Check on Bucket changes and update database - using (var dbContext = new DatabaseContext(_dbOptions)) - { - var dbBucket = dbContext.Bucket.First(i => i.BucketId == Bucket.BucketId); - if (dbBucket.Name != Bucket.Name || - dbBucket.ColorCode != Bucket.ColorCode || - dbBucket.BucketGroupId != Bucket.BucketGroupId) + catch (Exception e) { - // BucketGroup update requires special handling as ViewModel needs to trigger reload - // to force re-rendering of Blazor Page - if (dbBucket.BucketGroupId != Bucket.BucketGroupId) forceViewModelReload = true; - - if (dbContext.UpdateBucket(Bucket) == 0) - return new Tuple(false, - $"Error during database update: Unable to update Bucket.{Environment.NewLine}" + - $"{Environment.NewLine}" + - $"Bucket ID: {Bucket.BucketId}"); + transaction.Rollback(); + ViewModelReloadRequired?.Invoke(this, new ViewModelReloadEventArgs(this)); + return new ViewModelOperationResult( + false, + $"Error during database update: {e.Message}", + true); } } + } + } - // Check on BucketVersion changes and create new BucketVersion - using (var dbContext = new DatabaseContext(_dbOptions)) + /// + /// Updates a record in the database based on object + /// + /// Creates also a new record in the database + /// Can trigger + /// Object which contains information and results of this method + private ViewModelOperationResult UpdateBucket() + { + var forceViewModelReload = false; + using (var dbContext = new DatabaseContext(_dbOptions)) + { + using (var transaction = dbContext.Database.BeginTransaction()) { - var dbBucketVersion = - dbContext.BucketVersion.First(i => i.BucketVersionId == BucketVersion.BucketVersionId); - if (dbBucketVersion.BucketType != BucketVersion.BucketType || - dbBucketVersion.BucketTypeXParam != BucketVersion.BucketTypeXParam || - dbBucketVersion.BucketTypeYParam != BucketVersion.BucketTypeYParam || - dbBucketVersion.BucketTypeZParam != BucketVersion.BucketTypeZParam || - dbBucketVersion.Notes != BucketVersion.Notes) + try { - using (var transaction = dbContext.Database.BeginTransaction()) + // Check on Bucket changes and update database + var dbBucket = dbContext.Bucket.First(i => i.BucketId == Bucket.BucketId); + if (dbBucket.Name != Bucket.Name || + dbBucket.ColorCode != Bucket.ColorCode || + dbBucket.BucketGroupId != Bucket.BucketGroupId) { - try + // BucketGroup update requires special handling as ViewModel needs to trigger reload + // to force re-rendering of Blazor Page + if (dbBucket.BucketGroupId != Bucket.BucketGroupId) forceViewModelReload = true; + + if (dbContext.UpdateBucket(Bucket) == 0) + throw new Exception($"Error during database update: Unable to update Bucket.{Environment.NewLine}" + + $"{Environment.NewLine}" + + $"Bucket ID: {Bucket.BucketId}"); + } + + // Check on BucketVersion changes and create new BucketVersion + var dbBucketVersion = + dbContext.BucketVersion.First(i => i.BucketVersionId == BucketVersion.BucketVersionId); + if (dbBucketVersion.BucketType != BucketVersion.BucketType || + dbBucketVersion.BucketTypeXParam != BucketVersion.BucketTypeXParam || + dbBucketVersion.BucketTypeYParam != BucketVersion.BucketTypeYParam || + dbBucketVersion.BucketTypeZParam != BucketVersion.BucketTypeZParam || + dbBucketVersion.Notes != BucketVersion.Notes) + { + if (dbContext.BucketVersion.Any(i => + i.BucketId == BucketVersion.BucketId && i.Version > BucketVersion.Version)) + throw new Exception("Cannot create new Version as already a newer Version exists"); + + if (BucketVersion.ValidFrom == _currentYearMonth) { - if (dbContext.BucketVersion.Any(i => i.BucketId == BucketVersion.BucketId && i.Version > BucketVersion.Version)) - throw new Exception("Cannot create new Version as already a newer Version exists"); - - var modifiedVersion = BucketVersion; - if (BucketVersion.ValidFrom == _currentYearMonth) - { - // Bucket Version modified in the same month, - // so just update the version instead of creating a new version - if (dbContext.UpdateBucketVersion(modifiedVersion) == 0) - throw new Exception($"Unable to update Bucket Version.{Environment.NewLine}" + - $"{Environment.NewLine}" + - $"Bucket Version ID: {modifiedVersion.BucketVersionId}" + - $"Bucket ID: {modifiedVersion.BucketId}" + - $"Bucket Version: {modifiedVersion.Version}" + - $"Bucket Version Start Date: {modifiedVersion.ValidFrom.ToShortDateString()}"); - } - else - { - modifiedVersion.Version++; - modifiedVersion.BucketVersionId = 0; - modifiedVersion.ValidFrom = _currentYearMonth; - if (dbContext.CreateBucketVersion(modifiedVersion) == 0) - throw new Exception($"Unable to create new Bucket Version.{Environment.NewLine}" + - $"{Environment.NewLine}" + - $"Bucket ID: {modifiedVersion.BucketId}" + - $"Bucket Version: {modifiedVersion.Version}" + - $"Bucket Version Start Date: {modifiedVersion.ValidFrom.ToShortDateString()}"); - } - - transaction.Commit(); - //ViewModelReloadRequired?.Invoke(this); + // Bucket Version modified in the same month, + // so just update the version instead of creating a new version + if (dbContext.UpdateBucketVersion(BucketVersion) == 0) + throw new Exception($"Unable to update Bucket Version.{Environment.NewLine}" + + $"{Environment.NewLine}" + + $"Bucket Version ID: {BucketVersion.BucketVersionId}" + + $"Bucket ID: {BucketVersion.BucketId}" + + $"Bucket Version: {BucketVersion.Version}" + + $"Bucket Version Start Date: {BucketVersion.ValidFrom.ToShortDateString()}"); } - catch (Exception e) + else { - transaction.Rollback(); - ViewModelReloadRequired?.Invoke(this, new ViewModelReloadEventArgs(this)); - return new Tuple(false, $"Error during database update: {e.Message}"); + BucketVersion.Version++; + BucketVersion.BucketVersionId = 0; + BucketVersion.ValidFrom = _currentYearMonth; + if (dbContext.CreateBucketVersion(BucketVersion) == 0) + throw new Exception($"Unable to create new Bucket Version.{Environment.NewLine}" + + $"{Environment.NewLine}" + + $"Bucket ID: {BucketVersion.BucketId}" + + $"Bucket Version: {BucketVersion.Version}" + + $"Bucket Version Start Date: {BucketVersion.ValidFrom.ToShortDateString()}"); } } + transaction.Commit(); + if (forceViewModelReload) ViewModelReloadRequired?.Invoke(this, new ViewModelReloadEventArgs(this)); + return new ViewModelOperationResult(true, true); } - } + catch (Exception e) + { + transaction.Rollback(); + ViewModelReloadRequired?.Invoke(this, new ViewModelReloadEventArgs(this)); + return new ViewModelOperationResult( + false, + $"Error during database update: {e.Message}", + true); + } + } } - InModification = false; - CalculateValues(); - if (forceViewModelReload) ViewModelReloadRequired?.Invoke(this, new ViewModelReloadEventArgs(this)); - return new Tuple(true, string.Empty); } + /// + /// Triggers to cancel all modifications + /// public void CancelChanges() { InModification = false; ViewModelReloadRequired?.Invoke(this, new ViewModelReloadEventArgs(this)); // force Re-load to get old values back } - public Tuple HandleInOutInput(string key) + /// + /// Helper method to create a new record in the database based on User input + /// + /// Creation starts once Enter key is pressed + /// Recalculates figures after database operations + /// Pressed key + /// Object which contains information and results of this method + public ViewModelOperationResult HandleInOutInput(string key) { - if (key != "Enter") return new Tuple(true, string.Empty); + if (key != "Enter") return new ViewModelOperationResult(true); try { using (var dbContext = new DatabaseContext(_dbOptions)) @@ -580,12 +687,12 @@ public Tuple HandleInOutInput(string key) } //ViewModelReloadRequired?.Invoke(this); CalculateValues(); + return new ViewModelOperationResult(true); } catch (Exception e) { - return new Tuple(false, $"Error during database update: {e.Message}"); + return new ViewModelOperationResult(false, $"Error during database update: {e.Message}"); } - return new Tuple(true, string.Empty); } } } diff --git a/OpenBudgeteer.Core/ViewModels/ItemViewModels/MappingRuleViewModelItem.cs b/OpenBudgeteer.Core/ViewModels/ItemViewModels/MappingRuleViewModelItem.cs index cb1345b..acac5a6 100644 --- a/OpenBudgeteer.Core/ViewModels/ItemViewModels/MappingRuleViewModelItem.cs +++ b/OpenBudgeteer.Core/ViewModels/ItemViewModels/MappingRuleViewModelItem.cs @@ -2,7 +2,7 @@ using System.Collections.Generic; using System.Text; using Microsoft.EntityFrameworkCore; -using OpenBudgeteer.Core.Common; +using OpenBudgeteer.Core.Common.Database; using OpenBudgeteer.Core.Models; namespace OpenBudgeteer.Core.ViewModels.ItemViewModels @@ -10,6 +10,9 @@ namespace OpenBudgeteer.Core.ViewModels.ItemViewModels public class MappingRuleViewModelItem : ViewModelBase { private MappingRule _mappingRule; + /// + /// Reference to model object in the database + /// public MappingRule MappingRule { get => _mappingRule; @@ -17,6 +20,9 @@ public MappingRule MappingRule } private string _ruleOutput; + /// + /// Helper property to generate a readable output for + /// public string RuleOutput { get => _ruleOutput; @@ -25,17 +31,29 @@ public string RuleOutput private readonly DbContextOptions _dbOptions; + /// + /// Basic constructor + /// + /// Options to connect to a database public MappingRuleViewModelItem(DbContextOptions dbOptions) { _dbOptions = dbOptions; } + /// + /// Initialize ViewModel with an existing object + /// + /// Options to connect to a database + /// MappingRule instance public MappingRuleViewModelItem(DbContextOptions dbOptions, MappingRule mappingRule) : this(dbOptions) { MappingRule = mappingRule; GenerateRuleOutput(); } + /// + /// Translates object into a readable format + /// public void GenerateRuleOutput() { RuleOutput = MappingRule == null ? string.Empty : diff --git a/OpenBudgeteer.Core/ViewModels/ItemViewModels/MonthlyBucketExpensesReportViewModelItem.cs b/OpenBudgeteer.Core/ViewModels/ItemViewModels/MonthlyBucketExpensesReportViewModelItem.cs index f6cdaa5..b167ab0 100644 --- a/OpenBudgeteer.Core/ViewModels/ItemViewModels/MonthlyBucketExpensesReportViewModelItem.cs +++ b/OpenBudgeteer.Core/ViewModels/ItemViewModels/MonthlyBucketExpensesReportViewModelItem.cs @@ -5,9 +5,15 @@ namespace OpenBudgeteer.Core.ViewModels.ItemViewModels { + /// + /// Helper class for Reports showing monthly Bucket expenses + /// public class MonthlyBucketExpensesReportViewModelItem : ViewModelBase { private string _bucketName; + /// + /// Name of the Bucket + /// public string BucketName { get => _bucketName; @@ -15,12 +21,18 @@ public string BucketName } private ObservableCollection> _monthlyResults; + /// + /// Collection of the results for the report + /// public ObservableCollection> MonthlyResults { get => _monthlyResults; set => Set(ref _monthlyResults, value); } + /// + /// Basic constructor + /// public MonthlyBucketExpensesReportViewModelItem() { MonthlyResults = new ObservableCollection>(); diff --git a/OpenBudgeteer.Core/ViewModels/ItemViewModels/PartialBucketViewModelItem.cs b/OpenBudgeteer.Core/ViewModels/ItemViewModels/PartialBucketViewModelItem.cs index d7e5f2c..d518cec 100644 --- a/OpenBudgeteer.Core/ViewModels/ItemViewModels/PartialBucketViewModelItem.cs +++ b/OpenBudgeteer.Core/ViewModels/ItemViewModels/PartialBucketViewModelItem.cs @@ -1,5 +1,5 @@ using Microsoft.EntityFrameworkCore; -using OpenBudgeteer.Core.Common; +using OpenBudgeteer.Core.Common.Database; using OpenBudgeteer.Core.Common.EventClasses; using OpenBudgeteer.Core.Models; using System; @@ -11,11 +11,14 @@ namespace OpenBudgeteer.Core.ViewModels.ItemViewModels { /// - /// ViewModel to handle the multi-assignment of Buckets to one + /// Helper ViewModel to handle the multi-assignment of Buckets to one /// public class PartialBucketViewModelItem : ViewModelBase { private Bucket _selectedBucket; + /// + /// Affected Bucket + /// public Bucket SelectedBucket { get => _selectedBucket; @@ -23,6 +26,9 @@ public Bucket SelectedBucket } private string _selectedBucketOutput; + /// + /// Helper property to generate an output for the Bucket including the assigned amount + /// public string SelectedBucketOutput { get => _selectedBucketOutput; @@ -30,6 +36,9 @@ public string SelectedBucketOutput } private decimal _amount; + /// + /// Money that will be assigned to this Bucket + /// public decimal Amount { get => _amount; @@ -41,25 +50,38 @@ public decimal Amount } private ObservableCollection _availableBuckets; + /// + /// Helper collection with all available Buckets + /// public ObservableCollection AvailableBuckets { get => _availableBuckets; set => Set(ref _availableBuckets, value); } + /// + /// EventHandler which should be invoked once amount assigned to this Bucket has been changed. Can be used + /// to start further consistency checks and other calculations based on this change + /// public event EventHandler AmountChanged; + /// + /// EventHandler which should be invoked in case this instance should start its deletion process. Can be used + /// in case the way how this instance will be deleted is handled outside of this class + /// public event EventHandler DeleteAssignmentRequest; - public PartialBucketViewModelItem(DbContextOptions dbOptions, YearMonthSelectorViewModel yearMonthViewModel) + /// + /// Basic constructor + /// + /// Options to connect to a database + /// Current YearMonth + public PartialBucketViewModelItem(DbContextOptions dbOptions, DateTime yearMonth) { - AvailableBuckets = new ObservableCollection(); - // Add empty Bucket for empty pre-selection - AvailableBuckets.Add(new Bucket + AvailableBuckets = new ObservableCollection { - BucketId = 0, - BucketGroupId = 0, - Name = "No Selection" - }); + new Bucket {BucketId = 0, BucketGroupId = 0, Name = "No Selection"} + }; + // Add empty Bucket for empty pre-selection using (var dbContext = new DatabaseContext(dbOptions)) { foreach (var availableBucket in dbContext.Bucket.Where(i => i.BucketId <= 2)) @@ -69,9 +91,9 @@ public PartialBucketViewModelItem(DbContextOptions dbOptions, Y var query = dbContext.Bucket .Where(i => i.BucketId > 2 && - i.ValidFrom <= yearMonthViewModel.CurrentMonth && + i.ValidFrom <= yearMonth && (i.IsInactive == false || - (i.IsInactive && i.IsInactiveFrom > yearMonthViewModel.CurrentMonth))) + (i.IsInactive && i.IsInactiveFrom > yearMonth))) .OrderBy(i => i.Name); foreach (var availableBucket in query.ToList()) @@ -82,7 +104,14 @@ public PartialBucketViewModelItem(DbContextOptions dbOptions, Y SelectedBucket = AvailableBuckets.First(); } - public PartialBucketViewModelItem(DbContextOptions dbOptions, YearMonthSelectorViewModel yearMonthViewModel, Bucket bucket, decimal amount) : this(dbOptions, yearMonthViewModel) + /// + /// Initialize ViewModel based on an existing object and the final amount to be assigned + /// + /// Options to connect to a database + /// Current YearMonth + /// Bucket instance + /// Amount to be assigned to this Bucket + public PartialBucketViewModelItem(DbContextOptions dbOptions, DateTime yearMonth, Bucket bucket, decimal amount) : this(dbOptions, yearMonth) { Amount = amount; foreach (var availableBucket in AvailableBuckets) @@ -96,6 +125,9 @@ public PartialBucketViewModelItem(DbContextOptions dbOptions, Y if (SelectedBucket == null) SelectedBucket = AvailableBuckets.First(); } + /// + /// Triggers + /// public void DeleteBucket() { DeleteAssignmentRequest?.Invoke(this, new DeleteAssignmentRequestArgs(this)); diff --git a/OpenBudgeteer.Core/ViewModels/ItemViewModels/RuleSetViewModelItem.cs b/OpenBudgeteer.Core/ViewModels/ItemViewModels/RuleSetViewModelItem.cs index 30f0d55..a920d93 100644 --- a/OpenBudgeteer.Core/ViewModels/ItemViewModels/RuleSetViewModelItem.cs +++ b/OpenBudgeteer.Core/ViewModels/ItemViewModels/RuleSetViewModelItem.cs @@ -5,6 +5,7 @@ using System.Text; using Microsoft.EntityFrameworkCore; using OpenBudgeteer.Core.Common; +using OpenBudgeteer.Core.Common.Database; using OpenBudgeteer.Core.Models; namespace OpenBudgeteer.Core.ViewModels.ItemViewModels @@ -12,6 +13,9 @@ namespace OpenBudgeteer.Core.ViewModels.ItemViewModels public class RuleSetViewModelItem : ViewModelBase { private BucketRuleSet _ruleSet; + /// + /// Reference to model object in the database + /// public BucketRuleSet RuleSet { get => _ruleSet; @@ -19,14 +23,19 @@ public BucketRuleSet RuleSet } private Bucket _targetBucket; + /// + /// Bucket to which this RuleSet applies + /// public Bucket TargetBucket { get => _targetBucket; set => Set(ref _targetBucket, value); } - private bool _inModification; + /// + /// Helper property to check if the RuleSet is currently modified + /// public bool InModification { get => _inModification; @@ -34,6 +43,9 @@ public bool InModification } private bool _isHovered; + /// + /// Helper property to check if the cursor hovers over the entry in the UI + /// public bool IsHovered { get => _isHovered; @@ -41,6 +53,9 @@ public bool IsHovered } private ObservableCollection _mappingRules; + /// + /// Collection of MappingRules assigned to this RuleSet + /// public ObservableCollection MappingRules { get => _mappingRules; @@ -48,6 +63,9 @@ public ObservableCollection MappingRules } private ObservableCollection _availableBuckets; + /// + /// Helper collection to list all existing Buckets + /// public ObservableCollection AvailableBuckets { get => _availableBuckets; @@ -57,16 +75,16 @@ public ObservableCollection AvailableBuckets private readonly DbContextOptions _dbOptions; private RuleSetViewModelItem _oldRuleSetViewModelItem; - public RuleSetViewModelItem() + /// + /// Basic constructor + /// + /// Options to connect to a database + public RuleSetViewModelItem(DbContextOptions dbOptions) { MappingRules = new ObservableCollection(); AvailableBuckets = new ObservableCollection(); RuleSet = new BucketRuleSet(); TargetBucket = new Bucket(); - } - - public RuleSetViewModelItem(DbContextOptions dbOptions) : this() - { _dbOptions = dbOptions; AvailableBuckets.Add(new Bucket { @@ -92,6 +110,11 @@ public RuleSetViewModelItem(DbContextOptions dbOptions) : this( } } + /// + /// Initialize ViewModel based on an existing + /// + /// Options to connect to a database + /// RuleSet instance public RuleSetViewModelItem(DbContextOptions dbOptions, BucketRuleSet bucketRuleSet) : this(dbOptions) { @@ -113,12 +136,18 @@ public RuleSetViewModelItem(DbContextOptions dbOptions, BucketR } } + /// + /// Helper method to start modification process + /// public void StartModification() { _oldRuleSetViewModelItem = new RuleSetViewModelItem(_dbOptions, RuleSet); InModification = true; } + /// + /// Stops modification process and restores old values + /// public void CancelModification() { RuleSet = _oldRuleSetViewModelItem.RuleSet; @@ -127,14 +156,20 @@ public void CancelModification() _oldRuleSetViewModelItem = null; } + /// + /// Creates an initial and adds it to the + /// public void AddEmptyMappingRule() { MappingRules.Add(new MappingRuleViewModelItem(_dbOptions, new MappingRule())); } - public Tuple CreateUpdateRuleSetItem() + /// + /// Creates or updates records in the database based on and objects + /// + /// Object which contains information and results of this method + public ViewModelOperationResult CreateUpdateRuleSetItem() { - var result = new Tuple(true, string.Empty); using (var dbContext = new DatabaseContext(_dbOptions)) { using (var dbTransaction = dbContext.Database.BeginTransaction()) @@ -167,23 +202,28 @@ public Tuple CreateUpdateRuleSetItem() dbContext.CreateMappingRules(MappingRules.Select(i => i.MappingRule).ToList()); dbTransaction.Commit(); + _oldRuleSetViewModelItem = null; + InModification = false; + + return new ViewModelOperationResult(true); } catch (Exception e) { dbTransaction.Rollback(); - result = new Tuple(false, $"Errors during database update: {e.Message}"); + return new ViewModelOperationResult(false, $"Errors during database update: {e.Message}"); } } } - _oldRuleSetViewModelItem = null; - InModification = false; - - return result; } + /// + /// Deletes passed MappingRule from the collection + /// + /// MappingRule that needs to be removed public void DeleteMappingRule(MappingRuleViewModelItem mappingRule) { + //Note: Doesn't require any database updates as this will be done during CreateUpdateRuleSetItem MappingRules.Remove(mappingRule); if (MappingRules.Count == 0) AddEmptyMappingRule(); } diff --git a/OpenBudgeteer.Core/ViewModels/ItemViewModels/TransactionViewModelItem.cs b/OpenBudgeteer.Core/ViewModels/ItemViewModels/TransactionViewModelItem.cs index 19f7106..5816f83 100644 --- a/OpenBudgeteer.Core/ViewModels/ItemViewModels/TransactionViewModelItem.cs +++ b/OpenBudgeteer.Core/ViewModels/ItemViewModels/TransactionViewModelItem.cs @@ -1,5 +1,5 @@ using Microsoft.EntityFrameworkCore; -using OpenBudgeteer.Core.Common; +using OpenBudgeteer.Core.Common.Database; using OpenBudgeteer.Core.Common.EventClasses; using OpenBudgeteer.Core.Models; using System; @@ -8,15 +8,16 @@ using System.Linq; using System.Text; using System.Threading.Tasks; +using OpenBudgeteer.Core.Common; namespace OpenBudgeteer.Core.ViewModels.ItemViewModels { - /// - /// ViewModel for each Transaction Item - /// public class TransactionViewModelItem : ViewModelBase { private BankTransaction _transaction; + /// + /// Reference to model object in the database + /// public BankTransaction Transaction { get => _transaction; @@ -24,6 +25,9 @@ public BankTransaction Transaction } private Account _selectedAccount; + /// + /// Account where the Transaction is assigned to + /// public Account SelectedAccount { get => _selectedAccount; @@ -31,6 +35,9 @@ public Account SelectedAccount } private bool _inModification; + /// + /// Helper property to check if the Transaction is currently modified + /// public bool InModification { get => _inModification; @@ -38,6 +45,9 @@ public bool InModification } private bool _isHovered; + /// + /// Helper property to check if the cursor hovers over the entry in the UI + /// public bool IsHovered { get => _isHovered; @@ -45,6 +55,9 @@ public bool IsHovered } private ObservableCollection _buckets; + /// + /// Collection of Buckets which are assigned to this Transaction + /// public ObservableCollection Buckets { get => _buckets; @@ -52,18 +65,28 @@ public ObservableCollection Buckets } private ObservableCollection _availableAccounts; + /// + /// Helper collection to list all existing Account + /// public ObservableCollection AvailableAccounts { get => _availableAccounts; set => Set(ref _availableAccounts, value); } + /// + /// EventHandler which should be invoked in case the whole ViewModel has to be reloaded + /// e.g. due to various database record changes + /// public event EventHandler ViewModelReloadRequired; private readonly DbContextOptions _dbOptions; private readonly YearMonthSelectorViewModel _yearMonthViewModel; private TransactionViewModelItem _oldTransactionViewModelItem; + /// + /// Basic constructor + /// public TransactionViewModelItem() { Transaction = new BankTransaction(); @@ -71,6 +94,11 @@ public TransactionViewModelItem() AvailableAccounts = new ObservableCollection(); } + /// + /// Basic constructor + /// + /// Options to connect to a database + /// YearMonth ViewModel instance public TransactionViewModelItem(DbContextOptions dbOptions, YearMonthSelectorViewModel yearMonthViewModel) : this() { _dbOptions = dbOptions; @@ -96,6 +124,13 @@ public TransactionViewModelItem(DbContextOptions dbOptions, Yea SelectedAccount = AvailableAccounts.First(); } + /// + /// Initialize ViewModel with an existing object + /// + /// Options to connect to a database + /// YearMonth ViewModel instance + /// Transaction instance + /// Include assigned Buckets public TransactionViewModelItem(DbContextOptions dbOptions, YearMonthSelectorViewModel yearMonthViewModel, BankTransaction transaction, bool withBuckets = true) : this(dbOptions, yearMonthViewModel) { if (withBuckets) @@ -114,7 +149,7 @@ public TransactionViewModelItem(DbContextOptions dbOptions, Yea using (var bucketDbContext = new DatabaseContext(_dbOptions)) { var newItem = new PartialBucketViewModelItem(_dbOptions, - _yearMonthViewModel, + _yearMonthViewModel.CurrentMonth, bucketDbContext.Bucket.FirstOrDefault(i => i.BucketId == assignedBucket.BucketId), assignedBucket.Amount); newItem.SelectedBucketOutput = @@ -126,7 +161,7 @@ public TransactionViewModelItem(DbContextOptions dbOptions, Yea else { // Most likely an imported Transaction where Bucket assignment still needs to be done - var newItem = new PartialBucketViewModelItem(_dbOptions, _yearMonthViewModel, new Bucket() { BucketId = 0 }, transaction.Amount); + var newItem = new PartialBucketViewModelItem(_dbOptions, _yearMonthViewModel.CurrentMonth, new Bucket() { BucketId = 0 }, transaction.Amount); Buckets.Add(newItem); } } @@ -165,6 +200,10 @@ public TransactionViewModelItem(DbContextOptions dbOptions, Yea } + /// + /// Initialize ViewModel and transform passed into a + /// + /// BucketMovement which will be transformed public TransactionViewModelItem(BucketMovement bucketMovement) : this() { // Simulate a BankTransaction based on BucketMovement @@ -187,21 +226,47 @@ public TransactionViewModelItem(BucketMovement bucketMovement) : this() }; } + /// + /// Initialize and return a new ViewModel based on an existing object including + /// assigned Buckets + /// + /// Options to connect to a database + /// YearMonth ViewModel instance + /// Transaction instance + /// New ViewModel instance public static async Task CreateAsync(DbContextOptions dbOptions, YearMonthSelectorViewModel yearMonthViewModel, BankTransaction transaction) { return await Task.Run(() => new TransactionViewModelItem(dbOptions, yearMonthViewModel, transaction)); } + /// + /// Initialize and return a new ViewModel based on an existing object without + /// assigned Buckets + /// + /// Options to connect to a database + /// YearMonth ViewModel instance + /// Transaction instance + /// New ViewModel instance public static async Task CreateWithoutBucketsAsync(DbContextOptions dbOptions, YearMonthSelectorViewModel yearMonthViewModel, BankTransaction transaction) { return await Task.Run(() => new TransactionViewModelItem(dbOptions, yearMonthViewModel, transaction, false)); } + /// + /// Create and return a new ViewModel and transform passed into a + /// + /// BucketMovement which will be transformed + /// New ViewModel instance public static async Task CreateFromBucketMovementAsync(BucketMovement bucketMovement) { return await Task.Run(() => new TransactionViewModelItem(bucketMovement)); } + /// + /// Event that checks amount for all assigned Buckets and creates or removes an "empty item" + /// + /// Object that has triggered the event + /// Event Arguments about changed amount private void CheckBucketAssignments(object sender, AmountChangedArgs changedArgs) { // Check if this current event was triggered while updating the amount for the "emptyItem" @@ -257,6 +322,11 @@ private void CheckBucketAssignments(object sender, AmountChangedArgs changedArgs } } + /// + /// Event that handles the deletion of teh requested Bucket + /// + /// Object that has triggered the event + /// Event Arguments about deletion request private void DeleteRequestedBucketAssignment(object sender, DeleteAssignmentRequestArgs args) { // Prevent deletion all last remaining BucketAssignment @@ -266,55 +336,28 @@ private void DeleteRequestedBucketAssignment(object sender, DeleteAssignmentRequ } } + /// + /// Creates an "empty item" with the passed amount + /// + /// Amount that will be assigned to the Bucket private void AddEmptyBucketItem(decimal amount) { // All items have a valid Bucket assignment, create a new "empty item" - var emptyItem = new PartialBucketViewModelItem(_dbOptions, _yearMonthViewModel, new Bucket(), amount); + var emptyItem = new PartialBucketViewModelItem(_dbOptions, _yearMonthViewModel.CurrentMonth, new Bucket(), amount); emptyItem.AmountChanged += CheckBucketAssignments; emptyItem.DeleteAssignmentRequest += DeleteRequestedBucketAssignment; Buckets.Add(emptyItem); } - private Tuple CreateUpdateTransaction() + /// + /// Creates or updates a record in the database based on object + /// + /// (Re)Creates also records for each assigned Bucket + /// Object which contains information and results of this method + private ViewModelOperationResult CreateOrUpdateTransaction() { - // Consistency and Validity Checks - if (Transaction == null) return new Tuple(false, "Errors in Transaction object."); - if (SelectedAccount == null || SelectedAccount.AccountId == 0) return new Tuple(false, "No Bank account selected."); - //if (string.IsNullOrEmpty(Transaction.TransactionDate)) - // return new Tuple(false, "Transaction date is missing."); - //if (string.IsNullOrEmpty(transaction.Transaction.Memo)) - // return new Tuple(false, "Transaction Memo is missing."); - if (Buckets.Count == 0) - return new Tuple(false, "No Bucket assigned to this Transaction."); - - decimal assignedAmount = 0; - var skipBucketAssignment = false; - foreach (var assignedBucket in Buckets) - { - if (assignedBucket.SelectedBucket == null) - { - return new Tuple(false, "Pending Bucket assignment for this Transaction."); - } - - if (assignedBucket.SelectedBucket.BucketId == 0) - { - if (assignedBucket.SelectedBucket.Name == "No Selection") - { - // Imported Transaction where Bucket assignment is pending - // Allow Transaction Update but Skip DB Updates for Bucket assignment - skipBucketAssignment = true; - } - else - { - return new Tuple(false, "Pending Bucket assignment for this Transaction."); - } - } - assignedAmount += assignedBucket.Amount; - } - - if (assignedAmount != Transaction.Amount) - return new Tuple(false, - "Amount between Bucket assignment and Transaction not consistent."); + var result = PerformConsistencyCheck(out var skipBucketAssignment); + if (!result.IsSuccessful) return result; using (var dbContext = new DatabaseContext(_dbOptions)) { @@ -332,7 +375,6 @@ private Tuple CreateUpdateTransaction() // Delete all previous bucket assignments for transaction var budgetedTransactions = dbContext.BudgetedTransaction - .Where(i => i.TransactionId == transactionId); dbContext.DeleteBudgetedTransactions(budgetedTransactions); } @@ -360,19 +402,66 @@ private Tuple CreateUpdateTransaction() } transaction.Commit(); + return new ViewModelOperationResult(true); } catch (Exception e) { transaction.Rollback(); - return new Tuple(false, $"Errors during database update: {e.Message}"); + return new ViewModelOperationResult(false, $"Errors during database update: {e.Message}"); } } } + } - return new Tuple(true, string.Empty); + /// + /// Executes several data consistency checks (e.g. Bucket assignment, pending amount etc.) to see if changes + /// can be stored in the database + /// + /// Exclude checks on Bucket assignment + /// Object which contains information and results of this method + private ViewModelOperationResult PerformConsistencyCheck(out bool skipBucketAssignment) + { + decimal assignedAmount = 0; + skipBucketAssignment = false; + + // Consistency and Validity Checks + if (Transaction == null) return new ViewModelOperationResult(false, "Errors in Transaction object."); + if (SelectedAccount == null || SelectedAccount.AccountId == 0) return new ViewModelOperationResult(false, "No Bank account selected."); + if (Buckets.Count == 0) return new ViewModelOperationResult(false, "No Bucket assigned to this Transaction."); + + foreach (var assignedBucket in Buckets) + { + if (assignedBucket.SelectedBucket == null) + return new ViewModelOperationResult(false, "Pending Bucket assignment for this Transaction."); + + if (assignedBucket.SelectedBucket.BucketId == 0) + { + if (assignedBucket.SelectedBucket.Name == "No Selection") + { + // Imported Transaction where Bucket assignment is pending + // Allow Transaction Update but Skip DB Updates for Bucket assignment + skipBucketAssignment = true; + } + else + { + return new ViewModelOperationResult(false, "Pending Bucket assignment for this Transaction."); + } + } + + assignedAmount += assignedBucket.Amount; + } + + if (assignedAmount != Transaction.Amount) return new ViewModelOperationResult(false, "Amount between Bucket assignment and Transaction not consistent."); + + return new ViewModelOperationResult(true); } - private Tuple DeleteTransaction() + /// + /// Removes a record in the database based on object + /// + /// Removes also all its assigned Buckets + /// Object which contains information and results of this method + private ViewModelOperationResult DeleteTransaction() { using (var dbContext = new DatabaseContext(_dbOptions)) { @@ -385,19 +474,19 @@ private Tuple DeleteTransaction() // Delete all previous bucket assignments for transaction var budgetedTransactions = dbContext.BudgetedTransaction - .Where(i => i.TransactionId == Transaction.TransactionId); dbContext.DeleteBudgetedTransactions(budgetedTransactions); + transaction.Commit(); + return new ViewModelOperationResult(true); } catch (Exception e) { transaction.Rollback(); - return new Tuple(false, $"Errors during database update: {e.Message}"); + return new ViewModelOperationResult(false, $"Errors during database update: {e.Message}"); } } } - return new Tuple(true, string.Empty); } public void StartModification() @@ -415,38 +504,37 @@ public void CancelModification() _oldTransactionViewModelItem = null; } - public Tuple CreateItem() + public ViewModelOperationResult CreateItem() { - Transaction.TransactionId = 0; // Triggers CREATE during CreateUpdateTransaction() - return CreateUpdateTransaction(); + Transaction.TransactionId = 0; // Triggers CREATE during CreateOrUpdateTransaction() + return CreateOrUpdateTransaction(); } - public Tuple UpdateItem() + public ViewModelOperationResult UpdateItem() { - if (Transaction.TransactionId < 1) - return new Tuple(false, "Transaction needs to be created first in database"); + if (Transaction.TransactionId < 1) return new ViewModelOperationResult(false, "Transaction needs to be created first in database"); - var (result, message) = CreateUpdateTransaction(); - if (!result) + var result = CreateOrUpdateTransaction(); + if (!result.IsSuccessful) { // Trigger page reload as DB Update was not successfully ViewModelReloadRequired?.Invoke(this, new ViewModelReloadEventArgs(this)); - return new Tuple(false, message); + return new ViewModelOperationResult(false, result.Message, true); } _oldTransactionViewModelItem = null; InModification = false; - return new Tuple(true, string.Empty); + return new ViewModelOperationResult(true); } - public Tuple DeleteItem() + public ViewModelOperationResult DeleteItem() { - var (result, message) = DeleteTransaction(); - if (!result) return new Tuple(false, message); + var result = DeleteTransaction(); + if (!result.IsSuccessful) return result; ViewModelReloadRequired?.Invoke(this, new ViewModelReloadEventArgs(this)); - return new Tuple(true, string.Empty); + return new ViewModelOperationResult(true, true); } public void ProposeBucket() @@ -454,7 +542,7 @@ public void ProposeBucket() var proposal = CheckMappingRules(); if (proposal == null) return; Buckets.Clear(); - Buckets.Add(new PartialBucketViewModelItem(_dbOptions, _yearMonthViewModel, proposal, Transaction.Amount)); + Buckets.Add(new PartialBucketViewModelItem(_dbOptions, _yearMonthViewModel.CurrentMonth, proposal, Transaction.Amount)); } private Bucket CheckMappingRules() diff --git a/OpenBudgeteer.Core/ViewModels/ReportViewModel.cs b/OpenBudgeteer.Core/ViewModels/ReportViewModel.cs index a778336..aaced8f 100644 --- a/OpenBudgeteer.Core/ViewModels/ReportViewModel.cs +++ b/OpenBudgeteer.Core/ViewModels/ReportViewModel.cs @@ -7,7 +7,7 @@ using System.Threading.Tasks; using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore.Migrations.Operations; -using OpenBudgeteer.Core.Common; +using OpenBudgeteer.Core.Common.Database; using OpenBudgeteer.Core.Models; using OpenBudgeteer.Core.ViewModels.ItemViewModels; @@ -17,11 +17,25 @@ public class ReportViewModel : ViewModelBase { private readonly DbContextOptions _dbOptions; + /// + /// Basic constructor + /// + /// Options to connect to a database public ReportViewModel(DbContextOptions dbOptions) { _dbOptions = dbOptions; } + /// + /// Loads a set of balances per month from the database + /// + /// Considers only within a month + /// Number of months that should be loaded + /// + /// Collection of containing + /// Item1: representing the month + /// Item2: representing the balance + /// public async Task>> LoadMonthBalancesAsync(int months = 24) { return await Task.Run(() => @@ -53,6 +67,16 @@ public async Task>> LoadMonthBalancesAsync(int mon }); } + /// + /// Loads a set of income and expenses per month from the database + /// + /// Number of months that should be loaded + /// + /// Collection of containing + /// Item1: representing the month + /// Item2: representing the income + /// Item3: representing the expenses + /// public async Task>> LoadMonthIncomeExpensesAsync(int months = 24) { return await Task.Run(() => @@ -100,6 +124,15 @@ public async Task>> LoadMonthIncomeExpens }); } + /// + /// Loads a set of income and expenses per year from the database + /// + /// Number of years that should be loaded + /// Collection of containing + /// Item1: representing the year + /// Item2: representing the income + /// Item3: representing the expenses + /// public async Task>> LoadYearIncomeExpensesAsync(int years = 5) { return await Task.Run(() => @@ -147,6 +180,16 @@ public async Task>> LoadYearIncomeExpense }); } + /// + /// Loads a set of balances per month from the database showing the progress of the overall bank balance + /// + /// Considers all from the past + /// Number of months that should be loaded + /// + /// Collection of containing + /// Item1: representing the month + /// Item2: representing the balance + /// public async Task>> LoadBankBalancesAsync(int months = 24) { return await Task.Run(() => @@ -171,6 +214,13 @@ public async Task>> LoadBankBalancesAsync(int mont }); } + /// + /// Loads a set of expenses of a per month from the database + /// + /// Number of months that should be loaded + /// + /// Collection of ViewModelItems containing information about a and its expenses per month + /// public async Task> LoadMonthExpensesBucketAsync(int month = 12) { return await Task.Run(() => diff --git a/OpenBudgeteer.Core/ViewModels/RulesViewModel.cs b/OpenBudgeteer.Core/ViewModels/RulesViewModel.cs index 0aeaf8f..bdef627 100644 --- a/OpenBudgeteer.Core/ViewModels/RulesViewModel.cs +++ b/OpenBudgeteer.Core/ViewModels/RulesViewModel.cs @@ -6,6 +6,7 @@ using System.Threading.Tasks; using Microsoft.EntityFrameworkCore; using OpenBudgeteer.Core.Common; +using OpenBudgeteer.Core.Common.Database; using OpenBudgeteer.Core.Common.EventClasses; using OpenBudgeteer.Core.Models; using OpenBudgeteer.Core.ViewModels.ItemViewModels; @@ -15,6 +16,9 @@ namespace OpenBudgeteer.Core.ViewModels public class RulesViewModel : ViewModelBase { private RuleSetViewModelItem _newRuleSet; + /// + /// Helper property to handle setup of a new + /// public RuleSetViewModelItem NewRuleSet { get => _newRuleSet; @@ -22,28 +26,39 @@ public RuleSetViewModelItem NewRuleSet } private ObservableCollection _ruleSets; + /// + /// Collection of all from the database + /// public ObservableCollection RuleSets { get => _ruleSets; set => Set(ref _ruleSets, value); } + /// + /// EventHandler which should be invoked in case the whole ViewModel has to be reloaded + /// e.g. due to various database record changes + /// public event EventHandler ViewModelReloadRequired; private readonly DbContextOptions _dbOptions; - public RulesViewModel() - { - RuleSets = new ObservableCollection(); - } - - public RulesViewModel(DbContextOptions dbOptions) : this() + /// + /// Basic constructor + /// + /// Options to connect to a database + public RulesViewModel(DbContextOptions dbOptions) { _dbOptions = dbOptions; + RuleSets = new ObservableCollection(); ResetNewRuleSet(); } - public async Task> LoadDataAsync() + /// + /// Initialize ViewModel and load data from database + /// + /// Object which contains information and results of this method + public async Task LoadDataAsync() { return await Task.Run(() => { @@ -57,30 +72,35 @@ public async Task> LoadDataAsync() RuleSets.Add(new RuleSetViewModelItem(_dbOptions, bucketRuleSet)); } } + + return new ViewModelOperationResult(true); } catch (Exception e) { - return new Tuple(false, $"Error during loading: {e.Message}"); + return new ViewModelOperationResult(false, $"Error during loading: {e.Message}"); } - return new Tuple(true, string.Empty); - }); } - public Tuple CreateNewRuleSet() + /// + /// Starts creation process based on + /// + /// Triggers + /// Object which contains information and results of this method + public ViewModelOperationResult CreateNewRuleSet() { NewRuleSet.RuleSet.BucketRuleSetId = 0; - var (result, message) = NewRuleSet.CreateUpdateRuleSetItem(); - if (!result) - { - return new Tuple(false, message); - } + var result = NewRuleSet.CreateUpdateRuleSetItem(); + if (!result.IsSuccessful) return result; ResetNewRuleSet(); ViewModelReloadRequired?.Invoke(this, new ViewModelReloadEventArgs(this)); - return new Tuple(true, string.Empty); + return new ViewModelOperationResult(true, true); } + /// + /// Helper method to reset values of + /// public void ResetNewRuleSet() { NewRuleSet = new RuleSetViewModelItem(_dbOptions); @@ -93,43 +113,55 @@ public void ResetNewRuleSet() })); } - public Tuple SaveRuleSetItem(RuleSetViewModelItem ruleSet) + /// + /// Starts Creation or Update process for the passed + /// + /// Updates collection + /// Instance that needs to be created or updated + /// Object which contains information and results of this method + public ViewModelOperationResult SaveRuleSetItem(RuleSetViewModelItem ruleSet) { var result = ruleSet.CreateUpdateRuleSetItem(); - if (!result.Item1) return result; + if (!result.IsSuccessful) return result; RuleSets = new ObservableCollection(RuleSets.OrderBy(i => i.RuleSet.Priority)); return result; } - public Tuple DeleteRuleSetItem(RuleSetViewModelItem ruleSet) + /// + /// Starts Deletion process for the passed including all its + /// + /// Updates collection + /// Instance that needs to be deleted + /// Object which contains information and results of this method + public ViewModelOperationResult DeleteRuleSetItem(RuleSetViewModelItem ruleSet) { - var result = new Tuple(true, string.Empty); - using (var dbContext = new DatabaseContext(_dbOptions)) { using (var dbTransaction = dbContext.Database.BeginTransaction()) { try { - dbContext.DeleteMappingRules(dbContext.MappingRule.Where(i => - i.BucketRuleSetId == ruleSet.RuleSet.BucketRuleSetId)); + dbContext.DeleteMappingRules(dbContext.MappingRule + .Where(i => i.BucketRuleSetId == ruleSet.RuleSet.BucketRuleSetId)); dbContext.DeleteBucketRuleSet(ruleSet.RuleSet); dbTransaction.Commit(); RuleSets.Remove(ruleSet); + + return new ViewModelOperationResult(true); } catch (Exception e) { dbTransaction.Rollback(); - return new Tuple(false, $"Errors during database update: {e.Message}"); + return new ViewModelOperationResult(false, $"Errors during database update: {e.Message}"); } } - } - - return result; } + /// + /// Helper method to start Modification process for all + /// public void EditAllRules() { foreach (var ruleSet in RuleSets) @@ -138,7 +170,12 @@ public void EditAllRules() } } - public Tuple SaveAllRules() + /// + /// Starts the Creation or Update process for all + /// + /// Updates collection + /// Object which contains information and results of this method + public ViewModelOperationResult SaveAllRules() { using (var dbTransaction = new DatabaseContext(_dbOptions).Database.BeginTransaction()) { @@ -146,21 +183,25 @@ public Tuple SaveAllRules() { foreach (var ruleSet in RuleSets) { - (bool success, string message) = ruleSet.CreateUpdateRuleSetItem(); - if (!success) throw new Exception(message); + var result = ruleSet.CreateUpdateRuleSetItem(); + if (!result.IsSuccessful) throw new Exception(result.Message); } dbTransaction.Commit(); RuleSets = new ObservableCollection(RuleSets.OrderBy(i => i.RuleSet.Priority)); + + return new ViewModelOperationResult(true); } catch (Exception e) { dbTransaction.Rollback(); - return new Tuple(false, e.Message); + return new ViewModelOperationResult(false, e.Message); } } - return new Tuple(true, string.Empty); } + /// + /// Triggers to cancel all changes to all + /// public void CancelAllRules() { ViewModelReloadRequired?.Invoke(this, new ViewModelReloadEventArgs(this)); diff --git a/OpenBudgeteer.Core/ViewModels/TransactionViewModel.cs b/OpenBudgeteer.Core/ViewModels/TransactionViewModel.cs index 6d040d4..d51e5fd 100644 --- a/OpenBudgeteer.Core/ViewModels/TransactionViewModel.cs +++ b/OpenBudgeteer.Core/ViewModels/TransactionViewModel.cs @@ -1,4 +1,4 @@ -using OpenBudgeteer.Core.Common; +using OpenBudgeteer.Core.Common.Database; using OpenBudgeteer.Core.Models; using System; using System.Collections.Generic; @@ -11,6 +11,7 @@ using System.Windows; using OpenBudgeteer.Core.ViewModels.ItemViewModels; using Microsoft.EntityFrameworkCore; +using OpenBudgeteer.Core.Common; using OpenBudgeteer.Core.Common.EventClasses; namespace OpenBudgeteer.Core.ViewModels @@ -18,6 +19,9 @@ namespace OpenBudgeteer.Core.ViewModels public class TransactionViewModel : ViewModelBase { private TransactionViewModelItem _newTransaction; + /// + /// Helper property to handle creation of a new + /// public TransactionViewModelItem NewTransaction { get => _newTransaction; @@ -25,17 +29,29 @@ public TransactionViewModelItem NewTransaction } private ObservableCollection _transactions; + /// + /// Collection of loaded Transactions + /// public ObservableCollection Transactions { get => _transactions; set => Set(ref _transactions, value); } + /// + /// EventHandler which should be invoked in case the whole ViewModel has to be reloaded + /// e.g. due to various database record changes + /// public event EventHandler ViewModelReloadRequired; private readonly DbContextOptions _dbOptions; private readonly YearMonthSelectorViewModel _yearMonthViewModel; + /// + /// Basic Constructor + /// + /// Options to connect to a database + /// ViewModel instance to handle selection of a year and month public TransactionViewModel(DbContextOptions dbOptions, YearMonthSelectorViewModel yearMonthViewModel) { _dbOptions = dbOptions; @@ -45,7 +61,11 @@ public TransactionViewModel(DbContextOptions dbOptions, YearMon //_yearMonthViewModel.SelectedYearMonthChanged += (sender) => { LoadData(); }; } - public async Task> LoadDataAsync() + /// + /// Initialize ViewModel and load data from database + /// + /// Object which contains information and results of this method + public async Task LoadDataAsync() { try { @@ -73,16 +93,24 @@ public async Task> LoadDataAsync() ViewModelReloadRequired?.Invoke(this, new ViewModelReloadEventArgs(args.ViewModel)); Transactions.Add(transaction); } + + return new ViewModelOperationResult(true); } } catch (Exception e) { - return new Tuple(false, $"Error during loading: {e.Message}"); + return new ViewModelOperationResult(false, $"Error during loading: {e.Message}"); } - return new Tuple(true, string.Empty); } - public async Task> LoadDataAsync(Bucket bucket, bool withMovements) + /// + /// Initialize ViewModel and load data from database but only for assigned to the + /// passed . Optionally will be transformed to + /// + /// Bucket for which Transactions should be loaded + /// Include which will be transformed to + /// Object which contains information and results of this method + public async Task LoadDataAsync(Bucket bucket, bool withMovements) { try { @@ -131,16 +159,23 @@ public async Task> LoadDataAsync(Bucket bucket, bool withMov ViewModelReloadRequired?.Invoke(this, new ViewModelReloadEventArgs(args.ViewModel)); Transactions.Add(transaction); } + + return new ViewModelOperationResult(true); } } catch (Exception e) { - return new Tuple(false, $"Error during loading: {e.Message}"); + return new ViewModelOperationResult(false, $"Error during loading: {e.Message}"); } - return new Tuple(true, string.Empty); } - public async Task> LoadDataAsync(Account account) + /// + /// Initialize ViewModel and load data from database but only for assigned to the + /// passed + /// + /// Account for which Transactions should be loaded + /// Object which contains information and results of this method + public async Task LoadDataAsync(Account account) { try { @@ -166,35 +201,44 @@ public async Task> LoadDataAsync(Account account) ViewModelReloadRequired?.Invoke(this, new ViewModelReloadEventArgs(args.ViewModel)); Transactions.Add(transaction); } + + return new ViewModelOperationResult(true); } } catch (Exception e) { - return new Tuple(false, $"Error during loading: {e.Message}"); + return new ViewModelOperationResult(false, $"Error during loading: {e.Message}"); } - return new Tuple(true, string.Empty); } - public Tuple CreateItem() + /// + /// Starts creation process based on + /// + /// Triggers + /// Object which contains information and results of this method + public ViewModelOperationResult CreateItem() { - NewTransaction.Transaction.TransactionId = 0; // Triggers CREATE during CreateUpdateTransaction() - var (result, message) = NewTransaction.CreateItem(); - if (!result) - { - return new Tuple(false, message); - } + NewTransaction.Transaction.TransactionId = 0; + var result = NewTransaction.CreateItem(); + if (!result.IsSuccessful) return result; ResetNewTransaction(); ViewModelReloadRequired?.Invoke(this, new ViewModelReloadEventArgs(this)); - return new Tuple(true, string.Empty); + return new ViewModelOperationResult(true, true); } + /// + /// Helper method to reset values of + /// public void ResetNewTransaction() { NewTransaction = new TransactionViewModelItem(_dbOptions, _yearMonthViewModel); - NewTransaction.Buckets.Add(new PartialBucketViewModelItem(_dbOptions, _yearMonthViewModel)); + NewTransaction.Buckets.Add(new PartialBucketViewModelItem(_dbOptions, _yearMonthViewModel.CurrentMonth)); } + /// + /// Helper method to start modification process for all Transactions + /// public void EditAllTransaction() { foreach (var transaction in Transactions) @@ -203,7 +247,11 @@ public void EditAllTransaction() } } - public Tuple SaveAllTransaction() + /// + /// Starts update process for all Transactions + /// + /// Object which contains information and results of this method + public ViewModelOperationResult SaveAllTransaction() { using (var dbTransaction = new DatabaseContext(_dbOptions).Database.BeginTransaction()) { @@ -211,25 +259,32 @@ public Tuple SaveAllTransaction() { foreach (var transaction in Transactions) { - (bool success, string message) = transaction.UpdateItem(); - if (!success) throw new Exception(message); + var result = transaction.UpdateItem(); + if (!result.IsSuccessful) throw new Exception(result.Message); } dbTransaction.Commit(); + return new ViewModelOperationResult(true); } catch (Exception e) { dbTransaction.Rollback(); - return new Tuple(false, e.Message); + return new ViewModelOperationResult(false, e.Message); } } - return new Tuple(true, string.Empty); } + /// + /// Triggers to cancel all changes to all Transactions + /// public void CancelAllTransaction() { ViewModelReloadRequired?.Invoke(this, new ViewModelReloadEventArgs(this)); } + /// + /// Starts process to propose the right for all Transactions + /// + /// Sets all Transactions into Modification Mode in case they have a "No Selection" Bucket public void ProposeBuckets() { foreach (var transaction in Transactions) diff --git a/OpenBudgeteer.Core/ViewModels/YearMonthSelectorViewModel.cs b/OpenBudgeteer.Core/ViewModels/YearMonthSelectorViewModel.cs index b953423..89d3736 100644 --- a/OpenBudgeteer.Core/ViewModels/YearMonthSelectorViewModel.cs +++ b/OpenBudgeteer.Core/ViewModels/YearMonthSelectorViewModel.cs @@ -8,18 +8,11 @@ namespace OpenBudgeteer.Core.ViewModels { public class YearMonthSelectorViewModel : ViewModelBase { - public YearMonthSelectorViewModel() - { - Months = new ObservableCollection(); - for (var i = 1; i < 13; i++) - { - Months.Add(i); - } - SelectedMonth = DateTime.Now.Month; - SelectedYear = DateTime.Now.Year; - } - private int _selectedMonth; + /// + /// Number of the current month + /// + /// Change triggers public int SelectedMonth { get => _selectedMonth; @@ -31,6 +24,10 @@ public int SelectedMonth } private int _selectedYear; + /// + /// Number of the current year + /// + /// Change triggers public int SelectedYear { get => _selectedYear; @@ -42,28 +39,65 @@ public int SelectedYear } private ObservableCollection _months; + /// + /// Helper collection which contains the number of all months + /// public ObservableCollection Months { get => _months; - set => Set(ref _months, value); + private set => Set(ref _months, value); } + /// + /// Returns the first day as based on and + /// public DateTime CurrentMonth => new DateTime(SelectedYear, SelectedMonth, 1); + /// + /// EventHandler which should be invoked once the a year and/or a month has been modified. To be used to trigger + /// ViewModel reloads which are dependent on this ViewModel + /// public event EventHandler SelectedYearMonthChanged; - private bool _yearMontIsChanging; + private bool _yearMontIsChanging; // prevents double invoke of SelectedYearMonthChanged + + /// + /// Basic constructor + /// + public YearMonthSelectorViewModel() + { + Months = new ObservableCollection(); + for (var i = 1; i < 13; i++) + { + Months.Add(i); + } + SelectedMonth = DateTime.Now.Month; + SelectedYear = DateTime.Now.Year; + } + /// + /// Moves to the previous month + /// + /// Triggers public void PreviousMonth() { UpdateYearMonth(CurrentMonth.AddMonths(-1)); } + /// + /// Moves to the next month + /// + /// Triggers public void NextMonth() { UpdateYearMonth(CurrentMonth.AddMonths(1)); } + /// + /// Sets the date to the passed + /// + /// New date + /// Triggers (only once) private void UpdateYearMonth(DateTime newYearMonth) { _yearMontIsChanging = true; diff --git a/OpenBudgeteer.sln b/OpenBudgeteer.sln index df48e7a..cba1bb4 100644 --- a/OpenBudgeteer.sln +++ b/OpenBudgeteer.sln @@ -3,15 +3,13 @@ Microsoft Visual Studio Solution File, Format Version 12.00 # Visual Studio Version 16 VisualStudioVersion = 16.0.29424.173 MinimumVisualStudioVersion = 10.0.40219.1 -Project("{D954291E-2A0B-460D-934E-DC6B0785DB48}") = "OpenBudgeteer.Core", "OpenBudgeteer.Core\OpenBudgeteer.Core.shproj", "{CC22A1E2-FA88-417C-A402-69526C33D2F4}" -EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "OpenBudgeteer.Blazor", "OpenBudgeteer.Blazor\OpenBudgeteer.Blazor.csproj", "{0A5C2CAD-5438-4D2C-9C17-1ADF5C9AA9D3}" EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "OpenBudgeteer.Core", "OpenBudgeteer.Core\OpenBudgeteer.Core.csproj", "{B2969681-AC0D-4570-9043-46DBDAF11B18}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "OpenBudgeteer.Core.Test", "OpenBudgeteer.Core.Test\OpenBudgeteer.Core.Test.csproj", "{2AFE22D2-18FA-4326-B011-B6E3D6365E6B}" +EndProject Global - GlobalSection(SharedMSBuildProjectFiles) = preSolution - OpenBudgeteer.Core\OpenBudgeteer.Core.projitems*{0a5c2cad-5438-4d2c-9c17-1adf5c9aa9d3}*SharedItemsImports = 5 - OpenBudgeteer.Core\OpenBudgeteer.Core.projitems*{cc22a1e2-fa88-417c-a402-69526c33d2f4}*SharedItemsImports = 13 - EndGlobalSection GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU Release|Any CPU = Release|Any CPU @@ -21,6 +19,14 @@ Global {0A5C2CAD-5438-4D2C-9C17-1ADF5C9AA9D3}.Debug|Any CPU.Build.0 = Debug|Any CPU {0A5C2CAD-5438-4D2C-9C17-1ADF5C9AA9D3}.Release|Any CPU.ActiveCfg = Release|Any CPU {0A5C2CAD-5438-4D2C-9C17-1ADF5C9AA9D3}.Release|Any CPU.Build.0 = Release|Any CPU + {B2969681-AC0D-4570-9043-46DBDAF11B18}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {B2969681-AC0D-4570-9043-46DBDAF11B18}.Debug|Any CPU.Build.0 = Debug|Any CPU + {B2969681-AC0D-4570-9043-46DBDAF11B18}.Release|Any CPU.ActiveCfg = Release|Any CPU + {B2969681-AC0D-4570-9043-46DBDAF11B18}.Release|Any CPU.Build.0 = Release|Any CPU + {2AFE22D2-18FA-4326-B011-B6E3D6365E6B}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {2AFE22D2-18FA-4326-B011-B6E3D6365E6B}.Debug|Any CPU.Build.0 = Debug|Any CPU + {2AFE22D2-18FA-4326-B011-B6E3D6365E6B}.Release|Any CPU.ActiveCfg = Release|Any CPU + {2AFE22D2-18FA-4326-B011-B6E3D6365E6B}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE From 75cfc7c7a0a30997a0351c626ef20698d801996f Mon Sep 17 00:00:00 2001 From: Axelander Date: Mon, 14 Dec 2020 10:48:11 +0100 Subject: [PATCH 02/50] Updated Version --- OpenBudgeteer.Blazor/Shared/NavMenu.razor | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/OpenBudgeteer.Blazor/Shared/NavMenu.razor b/OpenBudgeteer.Blazor/Shared/NavMenu.razor index 9656122..8fa9cad 100644 --- a/OpenBudgeteer.Blazor/Shared/NavMenu.razor +++ b/OpenBudgeteer.Blazor/Shared/NavMenu.razor @@ -54,7 +54,7 @@
From 2b90d80e67b0d6ce9b6aa1231298841e41a5f400 Mon Sep 17 00:00:00 2001 From: Axelander Date: Mon, 14 Dec 2020 09:48:31 +0000 Subject: [PATCH 03/50] Update README.md --- README.md | 14 +++++++++++++- 1 file changed, 13 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index f23e6c5..c4803e3 100644 --- a/README.md +++ b/README.md @@ -4,10 +4,11 @@ OpenBudgeteer is a budgeting app based on the Bucket Budgeting Principle and ins ## Installation (Docker) -You can use the pre-built Docker Image from [Docker Hub](https://hub.docker.com/r/axelander/openbudgeteer). It requires a connection to a MySQL database which can be achieved by passing the following variables: +You can use the pre-built Docker Image from [Docker Hub](https://hub.docker.com/r/axelander/openbudgeteer). It requires a connection to a `MySQL` database which can be achieved by passing the following variables: | Variable | Description | Example | | --- | --- | --- | +| Connection:Provider | Type of database that should be used | mysql | | Connection:Server | IP Address to MySQL Server | 192.168.178.100 | | Connection:Port| Port to MySQL Server | 3306 | | Connection:Database | Database name | MyOpenBudgeteerDb | @@ -16,6 +17,7 @@ You can use the pre-built Docker Image from [Docker Hub](https://hub.docker.com/ ``` docker run -d --name='openbudgeteer' \ + -e 'Connection:Provider'='mysql' \ -e 'Connection:Server'='192.168.178.100' \ -e 'Connection:Port'='3306' \ -e 'Connection:Database'='MyOpenBudgeteerDb' \ @@ -25,6 +27,15 @@ docker run -d --name='openbudgeteer' \ 'axelander/openbudgeteer:latest' ``` +Alternatively you can use a local `Sqlite` database using the below settings: + +``` +docker run -d --name='openbudgeteer' \ + -e 'Connection:Provider'='sqlite' \ + -v '/my/local/path:/app/database' \ + -p '6100:80/tcp' \ + 'axelander/openbudgeteer:latest' +``` If you don't change the Port Mapping you can access the App with Port `80`. Otherwise like above example it can be accessed with Port `6100` ### Pre-release Version (Docker) @@ -33,6 +44,7 @@ A Pre-Release version can be used with the Tag `pre-release` ``` docker run -d --name='openbudgeteer' \ + -e 'Connection:Provider'='mysql' \ -e 'Connection:Server'='192.168.178.100' \ -e 'Connection:Port'='3306' \ -e 'Connection:Database'='MyOpenBudgeteerDb' \ From d71b89402e784a8885ab054e31a00c1b19a89842 Mon Sep 17 00:00:00 2001 From: Axelander Date: Tue, 15 Dec 2020 13:11:40 +0000 Subject: [PATCH 04/50] Update CHANGELOG.md --- CHANGELOG.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 3b9a6c9..ff6a6e9 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,4 +1,4 @@ -### 1.3 (xxxx-xx-xx) +### 1.3 (2020-12-15) * [Add] Support for Sqlite databases #2 * [Add] Unit Tests (not full coverage yet) From a62f3727057f732d26b1d68da1f2ee50d2568005 Mon Sep 17 00:00:00 2001 From: Axelander Date: Mon, 21 Dec 2020 15:31:52 +0100 Subject: [PATCH 05/50] Added screenshots to README --- README.md | 19 ++++++++++++++++--- icon.png => assets/icon.png | Bin assets/screenshot1.png | Bin 0 -> 92352 bytes assets/screenshot2.png | Bin 0 -> 10470 bytes assets/screenshot3.png | Bin 0 -> 46054 bytes assets/screenshot4.png | Bin 0 -> 14880 bytes assets/screenshot5.png | Bin 0 -> 26752 bytes assets/screenshot6.png | Bin 0 -> 1366 bytes 8 files changed, 16 insertions(+), 3 deletions(-) rename icon.png => assets/icon.png (100%) create mode 100644 assets/screenshot1.png create mode 100644 assets/screenshot2.png create mode 100644 assets/screenshot3.png create mode 100644 assets/screenshot4.png create mode 100644 assets/screenshot5.png create mode 100644 assets/screenshot6.png diff --git a/README.md b/README.md index c4803e3..f32b693 100644 --- a/README.md +++ b/README.md @@ -2,6 +2,8 @@ OpenBudgeteer is a budgeting app based on the Bucket Budgeting Principle and inspired by [YNAB](https://www.youneedabudget.com) and [Buckets](https://www.budgetwithbuckets.com). The Core is based on .NET Core and the MVVM Pattern, the Front End uses Blazor Server. +![Screenshot 1](assets/screenshot1.png) + ## Installation (Docker) You can use the pre-built Docker Image from [Docker Hub](https://hub.docker.com/r/axelander/openbudgeteer). It requires a connection to a `MySQL` database which can be achieved by passing the following variables: @@ -60,18 +62,25 @@ docker run -d --name='openbudgeteer' \ The best way to start with OpenBudgeteer is to create at least on Bank Account on the `Account Page`. +![Screenshot 2](assets/screenshot2.png) + ### Import Transactions -After that export some Transactions from your Online Banking and import the data using `Import Page`. At the moment it support CSV files only but you can individually set the characters for delimiter and text qualifier. The respective settings and other options are shown once the CSV file has been uploaded. +After that export some Transactions from your Online Banking and import the data using `Import Page`. At the moment it support CSV files only but you can individually set the characters for delimiter and text qualifier. The respective settings and other options are shown once the CSV file has been uploaded. + +![Screenshot 3](assets/screenshot3.png) + You also need to create an initial Transaction which includes the Bank Balance on a certain date. It should be the previous day of the very first imported Transaction. You can do this on the `Transaction Page`. Example: -You have imported all Transactions starting 2020-01-01. To have the right Balances create a Transaction for 2019-12-31 and add as amount the Account Balance of this day. You can mark this Transaction as `Income` (see more explanation in section `Bucket Assignment`). +You have imported all Transactions starting 2020-01-01. To have the right Balances create a Transaction for 2019-12-31 and add as amount the Account Balance of this day. You can mark this Transaction as `Income` (see more explanation in section `Bucket Assignment`). + +![Screenshot 4](assets/screenshot4.png) ### Create Buckets -Once you have some Transactions imported you can start creating Buckets on the `Bucket Page`. If you don't know what kind of Buckets you need, maybe start with some Buckets for your monthly or even yearly expenses like Car Insurance, Property Taxes, Instalments etc. and Buckets for your regular needs like Groceries or Gas. You can also create a Bucket for your next big trip by putting some money into it every month. +Once you have some Transactions imported you can start creating Buckets on the `Bucket Page`. If you don't know what kind of Buckets you need, maybe start with some Buckets for your monthly or even yearly expenses like Car Insurance, Property Taxes, Instalments etc. and Buckets for your regular needs like Groceries or Gas. You can also create a Bucket for your next big trip by putting some money into it every month. If you are happy with your setup, put some money into your Buckets. You can do it manually or automatically if a Bucket has a Want for the current month. @@ -79,6 +88,8 @@ If you are happy with your setup, put some money into your Buckets. You can do i In the final step you assign your Transactions to certain Buckets. Go back to the `Transaction Page`, edit a Transaction and select an appropiate Bucket. You can also do a mass edit. If a Transaction belongs to more than one Bucket just reduce the assigned amount and you get automatically the option to assign the remaining amount to anthoer Bucket. +![Screenshot 5](assets/screenshot5.png) + Transactions which represent your (monthly) income can be assigned to the pre-defined `Income` Bucket. If you have transffered money from one Account to another you can use the `Transfer` Bucket. Please ensure that all `Transfer` Transaction have in total a 0 Balance to prevent data inconsistency and wrong calculations. Once all Transactions are assigned properly you can go back to the Bucket Overview to see if your Budget management is still fine or if you need to do some movements. You should always ensure that your Buckets don't have a negative Balance. Also your `Remaining Budget` should never be negative. @@ -88,3 +99,5 @@ Once all Transactions are assigned properly you can go back to the Bucket Overvi OpenBudgeteer has a built-in versioning for Buckets which enables a proper histroy view on previous months. If you modify a Bucket, like changing the Type or the Target Amount, it will create a new version for the current selected month. It is not recommended to change a Bucket in the past, a change between two Bucket Version is prevented. If you close a Bucket it will be marked as `Inactive` for the next month. This can be only done if the Bucket Balance is 0 to prevent wrong calculations. + +![Screenshot 6](assets/screenshot6.png) diff --git a/icon.png b/assets/icon.png similarity index 100% rename from icon.png rename to assets/icon.png diff --git a/assets/screenshot1.png b/assets/screenshot1.png new file mode 100644 index 0000000000000000000000000000000000000000..a80cfd73afef01fae6449c5d76168c52a99282eb GIT binary patch literal 92352 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

U>WicwpPH>}cc!8dv3U+O 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 GoBst*jnpv! literal 0 HcmV?d00001 diff --git a/assets/screenshot2.png b/assets/screenshot2.png new file mode 100644 index 0000000000000000000000000000000000000000..fd1a0fbf9034857e0dc0b153cab76e9bd6972a12 GIT binary patch literal 10470 zcmdsdc~p{X->&X8!?@BP+y&iR7{&$FI;_+9t)yYAn8@!Yes zIk$E5uFXk2HtwTsjNqcMrNbX2?mx@=v!W_<>mMZU5nvh)l>T7CkDkb$U zb&Jq*z2tgR$a$AADJhwj)sHkW_>H%eluDe1nd!x7_o)F&i3ZK{$gEINx(pb3dhvL+ zi%W_$sQsY#o{g0N^?Tbv0oUY!$F6G_+M2!lC2OPA?RHDV;QnhP-BxElpYD!TNh#fn zDBX8$Q=jSMyw@HF?H!8}!ck#Aa;AUa;EX0Xg|$Ul7!)O%e%r_@`N0$J7Blhi+I3P= zFEmN}q@+$E(j-&82~d@UB(>E6AT1@e>oxEfDXA0Z6@QhIy8cLeqm2y>u0OUE>sEi4A^zbwaGa7!Y52gQL@<_r zD9dL`6YT)fgGwrhBl$p>Na(^VJ82%WD0Ccn)yjN7lt%EMR;G_HW50;b3>kNRVPU{R zP7%g858f!~Z9##sj2CzuwMoH-qYP?dwgzbm$Gs5Im1kJjr-Ii^~8A69SQIF9; z0ApclIX22)@2q6x=uyr9B6U(A3$~4dsA_GHiQ+NG z?aBMVMO1-y&=JY1h$5X#Lc?eWn5Kfr=}gZcyc-`i|DlYb-El$>Q7ejpW34k2hVbkk zVTwtXideU8-o|f+%2^MI2{lh+?De* zCH`bkC~}(LIBc!2%(^vM!*}(Vs%JsOZiZJu%B86`MxTvdIqb>%WF)$fn6pTK>Qy7) zbVMjZE22|xjFog8Q9-1$S3YxKOGp|ycnBe#71V(^rC!BXc{ESF5cHykfjGa61|) z<>=9YLG5T4$hJKheGtQ;_!8@|hg)f^?HS$gjLP|0Z?vNs-z@2_Nh0-h!j{ase#JDx zFyo8s)s5){*9pvZMWfYO^Xhl^wVm}2_XS#!2c3U=B`Q;*gsv>rf2*rna*Z7b;JZ{@N1&#Wa1D|Y-oF2^vmXT$^4pT5aQjw3pOV!>R)Tu zYdQ|~lV@aK%^BrBwQ)=gZmeZ_vp5y>fb3+bIW-B@a#DejSUW@ERjZ`Ku@~54>F1;d zeOVJ-*0*TnA(ov9BR(hyaMskM{vLyUh~>BF?&*;(YtypK3G8imQgWMe71x)B7$0Wq ze}Avm;FD@s?{jCW9YW#7Edi-sE+~*I1ItJm(WBQ5Lkm&lMeYyZu+xU(cgRagNAZA< zA`G`1i+VLDS`u3Dpe9jI;9BuM6UW++#E?<}O$XH)Hn2skn}<6}ksG7a|6U9BURnJFT?ce`CarTR9!;1a|0o@Ae zXB6~_F{0t-r|6dPw!89zCb`3V?**;GGGqHbock4@IW8$O74b1%kvyfGPVxxsjp_N4 zAisIar!o4yo^_qx#}pbd0z@9{rO9Sbs{v}9h6t30i1E?PuFWIX3mlPm!|TxrA!70gz)j9$-l01(*$M&e;`aFL7K74Uf7NN4oc zE(V#xW^#@C4KJq=9(BMr)BH^++)MhpEa}scos&jRy6ccr{w*$i8)rkix+{BB}cT=gg#l#HR07AbNUWLu~(;gFx2IAf?waT7bm?a zZ*S_=NF^TKs*<}MoV~3_jMiCP+|exD^=ziN559NN)0vKiaKn5tIiU?HelNX_Y1M^n zg%a7{h#_SodkDY57UgA3lY9*;)Z z;Kiuc8f-VE6*j^#2J>1dhP99VCoD;W&efMQ2-oc{sRw0C_xSWW3}pe6f-nz$Xj`=g z$Cl-6Nh9oGMjultOT&hO=b!BEsI%Adr8U@wzGfA#|5UOkduTqQe zp5M(;X=qSL@EpYh#~_cgnKGPF-KC21Nd?L=tye%9a(g)^x^YtU0m<7z-n3X&;-rCS zt2ycpnu~}&P8!TJHBHdEyx}+pR>^^-E?~<8Sl%&P7b>S*j(5P`M|bPYoG4XqT9kbf z92eYO)LPxcifotC-l6=eqSV{alJ|J-SqJLUTs`Xj+XH%iE*m*vC9}^JI;SoXkD4qd zLDOcw^G_6klQ}WR(NQZgwKLo&y))#F&^?Qq_#!hF<^G7HkxWl~O;`*|Kf!ZE|6;HB zB2N6+6TF%8K@j($Mk%2aT<4j0)S4MTg6)5B1kqG|!kTmW>DGQrvPPg+1g2>CDb`6H zOxCVXz{gVG>w|(@!oNs`=b-c8(2)MZJC!1u*{IQ8Oa$Gn1fZU2zM zdB1GiSt}2NB3)MS6w7rU*0sBnyu>Z7cu0dyfHNi&&~BeUhQ;9)71+~Ej%H>Um^`?Y zx=Vycr_&A3GW#`v20r|V$1;nRVQLVY6N9P{5dX!EtSHJ0=91~PMlsoMbL*h<7o74T z2HqWxe>H-Ml4|f(W{oU$*Y|rj?h%|oV6jj z8qXLk-DM&ky6BAG-5~QNe550`(H^}sZIpZ!mw38nJ}O+Xe}(wmh!bOySI1<(TA|PP z?kS*sJ}QpTM0^3?xCo}nND|y?9WCZ)o#J(GP6i;Xfca}n^-!b525=j$m2XTEL-teLkq7Mlggg#KFj;?jUZkARS;&afWC@<5`XaI^%ePsIJfZ}{ zhMgLK&?^N`>_L0^^M$5;NG9bi*{B{k#x6}qF>k{9ljmV61wp+?JwBG}m2S16a6@2qC+E_#VA9C(Mx6~sT`#8!5T;~{E-TKjmgDBwPEe>OT&i{=jQ ztiQ?^shWsMH+aqNo??Z*_zyMlpA%*T59%|@-vFnzPSp(CY3e!dzqi4`pZpmIJ=`bR z-$x7HI-FNe^~!yN`B@naDluDXp48T2b7LE`q4oF9>dwc9yqgh~_1^T; zAK#g1aZXn-G^RX|vqhK91bt${h=C6i@5L7jNM1*FZWMCESCq5G_0)m;8BS_mh@s^H zcXo<@3En3``>GR{_mLAFnoDFw)aLPVa<)~4z{|Xm^C$s|_6iuZVLO#Fhb_2eR+)3u z7TbP(pasjXY@V;9rqa%0h3F{8Gs;}^KEDRzk>y&NInK7=7Iog$pw|b5yr{CSl`t2g zkwJuo28{3vX;0!l>K5@hBhNI0`3(yklO20nzWRo3>SCmZ@lV<QH+M{l7WYyDGd3~<{g9|aPaWrd4#)1G9%1p0(PrO7mnjAIBZoK% zqnr&w(f2(l&xtAugiI;sKVz~yv100pHs1jVswMI^Z`f~!T0`=?>Y@nzDD8APVC>q( zrK~RJ{^bwrUdJrAzVO@inrV(!e%8u1v{uZyZ&TYbveHVHp>o*};ICxjwS>S>jWGgr zKN(Km;)&myWO{ufIur1&Wg*y`82de3q7<@Pi%s}+SsfqdtubLrY&9#h#RxucRgUeC zk81(lq6)7WrDIDq87UOP)X5z%G%C0wjtU!-@0{?a&nDELo#!6rXp%h|^P0Ia4@y?D z#*Joo-VO^fqUdmTbar^_Lnb%uV*tVDSRUcX!z{QS>JGD@?~(iNIenw8oA1zy0#yPj zKELU<0UnZXdGlQjEMY>V-Of*AM6a}&FlP4{nOp~w=aE8lGIbLdQz!+H#lve14 z;EwR_PA&munv#9z;tQC2hbfK0#$XUziUR&_?CR!@p6^K|`eT{|`u0n!k4e^ zg_M=bIRc&iGtE@*zIJrb7~0DRQm2$^EmE(QoU4yPXEmVWo2`$wiPmkCoVmAkzFj?H zhwS=~9;pBEksTj8gAh+j=5RgbUwM-M#{>W8hxVT`!e^IE0udr)O;ZzvP1Cx{jU9(J z5p|VVU*|SRNp;eCG6;5d32fWMZsXbURLlmP(AF(uWhr^2^tWH6-gm%ip+}A0Is7{F zbVcvZ$kk;fiM7HBm)wj`dgts`FFzEWX(H)o&`1?g`cguH_Z=^RmG@T{MmI(KliF%T z{TifwB`Oj(CZtN`fAf$o28dg*;RVqmq9FGmiA{0*7Gcefrtbq&CgbiZxVlvjM=~D{ zdGL-hmp-wVfh`9jSFN7wF6*GPxzo%#%=7>TZHrbQ4XP`LXScV8M!#LM=gnJDPw**c zp5ITT$L2#Ahb`o$TG{u>&Qn}ZOYX*2dwxXr)XwPhktD$lDcSPMQ zSUhksMo=~(`dT^swIO>A3#;AG3UQcyDT_vFB3IfYvD z81)O=c6Xt+cZO##rV|$0pxRBVEv@|vfk9+@Hvka~(xy~B2e?Ld z`Le)|zhoHu^}lJ#+kqdg@e{00e$aVqI>BAV3f|DWqajvqw{?=5k=%p4j{|V1)*W)p zd{Io_FFNJws=upCrvX%y}S5q}dN2U6%|1sUC9AI@3EW!-2N?$*$ zgy|daf55U92YEm)w4nUavPnS=SU#&-14-R`89?Pl*-NapK_9!o@_S2J*6;;F64^JJX^2P$=j=!(<%1##+9pOLH0X7G3KehS^h`~0BZgih*gxKc4n^$k_5i_U(pR=jN{7t;G0k+_*OwA8imb-gw^c%p5(^F#-8Y#16MK88`{LwUn3nv+GhtoIczxoosi%9Mva^eq)DW1U$>Lap>OQ%FBh6zSMxaKU9n?`b(oS z(1{%HNQc;6`cq+3^#{=uXsy@_2XC3nuIyeUScCAer)vx4k~sgpH3K(N9!5*)Q!d94oGdq(aPdC+a|{DJCcLC;~5 zlG6U2GgGxJE-Uf7+@D%0WZy<5SQIV+#7zpNn@W zq4A_pU)91fUo{=u>_1m|L($5v^{uwEnnS@?g-W#EWy`=rWRZGVREM^IAEu-HbVIk; zQDQ`DZ|0=r_^mOy{fT@|zRH6`UMhdIv15+4ZRuznuF!xv7HT`hx$1N(V4@ZKHNwuR z_7zqm{rDxdhb(KbfsV#D@Ezv%it;_Xe^TKfG-Q_$Jom#HrgP|pGl1owy*-0;A|^6D z2WtPEd>Q+&0QzlnH&RS$s9^l@Yb}djJa^XqEOfjc`4e^x_l!ub?y$97>Nk z-K=cfdi??Ulu?UeWo@$XAVkFj*s_|60C~!W)7ChrhuSQ`uj|@*h(n7_r zcxk4On|{JfemRpjK+hC;qA;rV%vGs9KP|I=^^QX{U*R7sB45h3&-|FJkoV z=bTLxb4UXfxNR8(laa`Do+qT?N)W3W>Akz9){Okjig&GV-od3;EL_^|E=x{aYMZ{d z!W|)d1i}xn6n+AyLH5{>Aq~34FKT*`oEu3y))&|si0Zd`S=`@3Y9)I#yifY(p6Z|x znFQa;oQ%ZM8v@LOjC-1#Rf2baA2AB{H5#ZFO1eDX*v}LkV9BlZYMb{?LuJzeSne9F z2aAsz`OTj^E!RL_gJ$;i+(kRH&Og@lEAPM;hL_{7=A%k$8TWsMR{c}Tc{rvlZumH! z3RmNNo`TrSpvk#cMDca**$KbPH6;AxLYp%H6&83Xb%e$us zsN%}FxuGe6ASK*DRAj>Oj`EE?XQl;OL+7w+8o}gF1}01mfv*>x=M~=xj>7Lu*p<># z{!3l(ayWxb49hOwc$Rp>dF8p1;o~smn#@^rWe|ky*AvuIuR__R8!D$cCz4K|(1@yf zaIR5-kwqVWimg-i-%o8!Szl6tN1OD3UM+?PQ5ISpzlCGI&Oh_%huHpNUVh-`9E#I( z>j!Tax$r4|J(lD_(u9B;lvlAGmt|nHZF`ZEz%MA3e4duMy~59>^|TH)GyUD^{625S z!jLnQU7SXKjs2+BYHw!qGgF_A+RmO8tkAElEU1HgV-{_}J3dw`?X~-yEP8W6xyboj zWIM)pdD!v&@Z!Uss2sD=`JYPsr1>eqX6rBdW2gJH4k)ZvzbXg|HHp+4QP~BR)g}FV3`CF4;19dT z1P5;xU|wOTa&Z-!hFcg(wO7rweqW>h(=^iHf-J5*$w%ermlIS9hCC=um2D$=pggnc zUvtSZp{`@YO$jT8=uQq&kfxQGU0up#cPuthrYAV?;xx8O%ffeN$|va0c+iJOfC=%Q4LWVyAd_ z%4Bx{OOYwCv#c~Sw;RQotrZ15Pwl3Mi;xFk+kWQTWc%#Ju(<_WvrgkRs4R`qCJjIQ z^A5m%a`(ToX85P_@SiD!j*=m`1<*i|crd9m$>@DlLCstrjTn*t3>|J9I~8(76M>FD8gzZX!ayf^I}+-8(>)r) z7?)*C>`isYplnH#uMw6gxHm1F& zJW7}|R;mf*RRIqZm!@QwM~=-v!jXprj3fL2vLs-}*32PfWt|3M!tujvvjkSn^wSA$ zIfe#b%8kkM(+`tT$^3hrkXi@}=?i2JgTa}UUL$7|U@jZ>M4nW-aGUqibj9xXVdjVR z`^5T=S_skn%Vvebo{YlMkg+~vp__{85Gw&QBO{=L{rYletnnP8T*xrJt z5egZ{g*khuF-@NI>$}~@C6!QKmf#YAEYKF`>|q5!7WJU)bAsSC(zQyPptF2E=*ti@xJ!6Al_0hwk!qSk*P2> za!ssma}_{tZ3wyWB#^QwB9R7f*#dN1L0N~@@Ajb)o#=;2VRF#f=D+y<1sXy-WxPYS zR~=g6?48U!$J~d~#VRMrppTdb`&lb072eB0rDe26(#ZXhS3R???`fMO(Nne~xslKP z=5`kdD!XaT^Jfh=`;D!n6DFS>>`S;R88Xo~g!Pm#UNg$I15_PBY7$jZ?D-u+5xT5EsHyk z&r?{Ak^H@j`_N<*K+LJewm0R4H3 zWq&bw@Idy|Kt%gIMI%ec5M*g{1%MEiRy8KZC$8N%qH&eQN#pwQ7OzHIk(||(_=mpB(mc5L00lle{x8)xgGXL&L09;Am!D%#UTbtB$(!INJKkV zcA9oCR>=Mpw@dRWeIxx%kOG8@-jS}fH~O?WBZF||Q|o`!07(>e$7V2^I1q;3oW;qktqE_t6#mv*ir6RsXwc< xR^L=pTHW*i#c!wmf9q-g`*-;C#XX|LO}RG1xuy%dBtS}8nA@0@pZ56k-vF3p+ExGn literal 0 HcmV?d00001 diff --git a/assets/screenshot3.png b/assets/screenshot3.png new file mode 100644 index 0000000000000000000000000000000000000000..9ef89385e779da7f367b8812f12e57d3f91a792c GIT binary patch literal 46054 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È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 z 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 z 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 YU+#HL+vw4g*#H0l07*qoM6N<$g0OIv5&!@I literal 0 HcmV?d00001 From 729d5af245f427bdf1b34238887a69b7ae1a8e62 Mon Sep 17 00:00:00 2001 From: Axelander Date: Tue, 22 Dec 2020 10:31:58 +0000 Subject: [PATCH 06/50] Update .gitignore Deleted .editorconfig --- .editorconfig | 4 ++++ CHANGELOG.md | 5 +++++ OpenBudgeteer.Blazor/Dockerfile | 4 ++-- OpenBudgeteer.Blazor/OpenBudgeteer.Blazor.csproj | 2 +- OpenBudgeteer.Blazor/Pages/Import.razor | 2 +- OpenBudgeteer.Blazor/Shared/NavMenu.razor | 2 +- OpenBudgeteer.Core.Test/OpenBudgeteer.Core.Test.csproj | 5 ++++- OpenBudgeteer.Core/OpenBudgeteer.Core.csproj | 2 +- OpenBudgeteer.sln | 7 ++++++- 9 files changed, 25 insertions(+), 8 deletions(-) create mode 100644 .editorconfig diff --git a/.editorconfig b/.editorconfig new file mode 100644 index 0000000..054ba94 --- /dev/null +++ b/.editorconfig @@ -0,0 +1,4 @@ +# To learn more about .editorconfig see https://aka.ms/editorconfigdocs + +[*.cs] +guidelines = 120 diff --git a/CHANGELOG.md b/CHANGELOG.md index ff6a6e9..82735ae 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,8 @@ +### 1.4 (20xx-xx-xx) + +* [Changed] Core and Blazor Frontend updated to .Net 5.0 +* [Changed] File Preview on Import Page now read-only + ### 1.3 (2020-12-15) * [Add] Support for Sqlite databases #2 diff --git a/OpenBudgeteer.Blazor/Dockerfile b/OpenBudgeteer.Blazor/Dockerfile index 025f14e..895a470 100644 --- a/OpenBudgeteer.Blazor/Dockerfile +++ b/OpenBudgeteer.Blazor/Dockerfile @@ -1,11 +1,11 @@ #See https://aka.ms/containerfastmode to understand how Visual Studio uses this Dockerfile to build your images for faster debugging. -FROM mcr.microsoft.com/dotnet/core/aspnet:3.1-buster-slim AS base +FROM mcr.microsoft.com/dotnet/aspnet:5.0-buster-slim AS base WORKDIR /app EXPOSE 80 EXPOSE 443 -FROM mcr.microsoft.com/dotnet/core/sdk:3.1-buster AS build +FROM mcr.microsoft.com/dotnet/sdk:5.0 AS build WORKDIR /src COPY ["OpenBudgeteer.Blazor/OpenBudgeteer.Blazor.csproj", "OpenBudgeteer.Blazor/"] COPY ["OpenBudgeteer.Core/OpenBudgeteer.Core.csproj", "OpenBudgeteer.Core/"] diff --git a/OpenBudgeteer.Blazor/OpenBudgeteer.Blazor.csproj b/OpenBudgeteer.Blazor/OpenBudgeteer.Blazor.csproj index 9755817..5b9aefa 100644 --- a/OpenBudgeteer.Blazor/OpenBudgeteer.Blazor.csproj +++ b/OpenBudgeteer.Blazor/OpenBudgeteer.Blazor.csproj @@ -1,7 +1,7 @@  - netcoreapp3.1 + net5.0 c146cdfd-f78c-4fb3-8be0-4e15e589371a Linux OpenBudgeteer diff --git a/OpenBudgeteer.Blazor/Pages/Import.razor b/OpenBudgeteer.Blazor/Pages/Import.razor index a677eeb..a36e9fe 100644 --- a/OpenBudgeteer.Blazor/Pages/Import.razor +++ b/OpenBudgeteer.Blazor/Pages/Import.razor @@ -272,7 +272,7 @@

File Content:
- +
@if (_isConfirmationModalDialogVisible) diff --git a/OpenBudgeteer.Blazor/Shared/NavMenu.razor b/OpenBudgeteer.Blazor/Shared/NavMenu.razor index 8fa9cad..65cfb46 100644 --- a/OpenBudgeteer.Blazor/Shared/NavMenu.razor +++ b/OpenBudgeteer.Blazor/Shared/NavMenu.razor @@ -54,7 +54,7 @@
diff --git a/OpenBudgeteer.Core.Test/OpenBudgeteer.Core.Test.csproj b/OpenBudgeteer.Core.Test/OpenBudgeteer.Core.Test.csproj index ae4a64b..a806ce2 100644 --- a/OpenBudgeteer.Core.Test/OpenBudgeteer.Core.Test.csproj +++ b/OpenBudgeteer.Core.Test/OpenBudgeteer.Core.Test.csproj @@ -1,9 +1,12 @@ - netcoreapp3.1 false + + 9 + + net5.0 diff --git a/OpenBudgeteer.Core/OpenBudgeteer.Core.csproj b/OpenBudgeteer.Core/OpenBudgeteer.Core.csproj index 47678e3..f62041e 100644 --- a/OpenBudgeteer.Core/OpenBudgeteer.Core.csproj +++ b/OpenBudgeteer.Core/OpenBudgeteer.Core.csproj @@ -1,7 +1,7 @@ - netcoreapp3.1 + net5.0 diff --git a/OpenBudgeteer.sln b/OpenBudgeteer.sln index cba1bb4..99fffd3 100644 --- a/OpenBudgeteer.sln +++ b/OpenBudgeteer.sln @@ -7,7 +7,12 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "OpenBudgeteer.Blazor", "Ope EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "OpenBudgeteer.Core", "OpenBudgeteer.Core\OpenBudgeteer.Core.csproj", "{B2969681-AC0D-4570-9043-46DBDAF11B18}" EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "OpenBudgeteer.Core.Test", "OpenBudgeteer.Core.Test\OpenBudgeteer.Core.Test.csproj", "{2AFE22D2-18FA-4326-B011-B6E3D6365E6B}" +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "OpenBudgeteer.Core.Test", "OpenBudgeteer.Core.Test\OpenBudgeteer.Core.Test.csproj", "{2AFE22D2-18FA-4326-B011-B6E3D6365E6B}" +EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution Items", "{86A25C9F-8B53-47CC-B032-AF705FE284A2}" + ProjectSection(SolutionItems) = preProject + .editorconfig = .editorconfig + EndProjectSection EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution From f674b0a3a813757b5040529a97e3304bdd3df4de Mon Sep 17 00:00:00 2001 From: Axelander Date: Tue, 22 Dec 2020 12:05:18 +0100 Subject: [PATCH 07/50] Misc small visual updates and fixes on Import Page --- CHANGELOG.md | 1 + OpenBudgeteer.Blazor/Pages/Import.razor | 103 +++++++++++++----------- 2 files changed, 55 insertions(+), 49 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 82735ae..433ff5e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,7 @@ * [Changed] Core and Blazor Frontend updated to .Net 5.0 * [Changed] File Preview on Import Page now read-only +* [Changed] Misc small visual updates and fixes on Import Page ### 1.3 (2020-12-15) diff --git a/OpenBudgeteer.Blazor/Pages/Import.razor b/OpenBudgeteer.Blazor/Pages/Import.razor index a36e9fe..1a08313 100644 --- a/OpenBudgeteer.Blazor/Pages/Import.razor +++ b/OpenBudgeteer.Blazor/Pages/Import.razor @@ -40,32 +40,33 @@
@if (_step4Enabled) { - + } @if (_dataContext.SelectedImportProfile != null && _dataContext.SelectedImportProfile.ImportProfileId != 0) { - - + + }
-
- -
+ + +
+
@@ -202,27 +203,28 @@ {
Step 4: Validate and Import Data
- + @if (_dataContext.ValidRecords > 0) { - + }
-

Total Records: @_dataContext.TotalRecords

-

Valid Records: @_dataContext.ValidRecords

-

Records with errors: @_dataContext.RecordsWithErrors

+ Total Records: @_dataContext.TotalRecords
+ Valid Records: @_dataContext.ValidRecords
+ Records with errors: @_dataContext.RecordsWithErrors @if (_validationErrorMessage != string.Empty) { -

Error message: @_validationErrorMessage

+ Error message: @_validationErrorMessage }
@if (_dataContext.ParsedRecords.Any(i => i.IsValid)) { -
Preview (Valid Records)
- - +
+
Preview (Valid Records)
+
+ @@ -230,43 +232,46 @@ - - + + @foreach (var transaction in _dataContext.ParsedRecords.Where(i => i.IsValid)) { - + - + } - -
Date AccountMemo Amount
@transaction.Result.TransactionDate.ToShortDateString()@transaction.Result.TransactionDate.ToShortDateString() @_dataContext.SelectedAccount.Name @transaction.Result.Payee @transaction.Result.Memo@transaction.Result.Amount@transaction.Result.Amount
+ + +
} @if (_dataContext.ParsedRecords.Any(i => !i.IsValid)) { -
Records with error:
- - +
+
Records with error:
+
+ - - @foreach (var transaction in _dataContext.ParsedRecords.Where(i => !i.IsValid)) - { - - - - - } -
Row Details
@transaction.RowIndex -
@transaction.Error.Value
-
@transaction.Error.UnmappedRow
-
+ + @foreach (var transaction in _dataContext.ParsedRecords.Where(i => !i.IsValid)) + { + + @transaction.RowIndex + +
@transaction.Error.Value
+
@transaction.Error.UnmappedRow
+ + + } + + } } From 89b8e29c318af09fa2bdd0968cc944cee2e71099 Mon Sep 17 00:00:00 2001 From: Axelander Date: Tue, 22 Dec 2020 14:11:43 +0100 Subject: [PATCH 08/50] Removed old using statements --- OpenBudgeteer.Blazor/Pages/Import.razor | 2 -- 1 file changed, 2 deletions(-) diff --git a/OpenBudgeteer.Blazor/Pages/Import.razor b/OpenBudgeteer.Blazor/Pages/Import.razor index 1a08313..8424401 100644 --- a/OpenBudgeteer.Blazor/Pages/Import.razor +++ b/OpenBudgeteer.Blazor/Pages/Import.razor @@ -3,8 +3,6 @@ @using OpenBudgeteer.Core.ViewModels @using OpenBudgeteer.Core.Common.Database @using Microsoft.EntityFrameworkCore -@using System.IO -@using System.Text @using OpenBudgeteer.Core.Common @using OpenBudgeteer.Core.Models @using Tewr.Blazor.FileReader From 0296812a1af8c4b9040bcc19d2dcc6f349a709aa Mon Sep 17 00:00:00 2001 From: Axelander Date: Tue, 22 Dec 2020 14:39:11 +0100 Subject: [PATCH 09/50] Consistent Chart Header styles on Report Page --- CHANGELOG.md | 1 + OpenBudgeteer.Blazor/Pages/Report.razor | 16 ++++++++++------ .../ViewModels/BlazorReportViewModel.cs | 6 ++---- OpenBudgeteer.Blazor/wwwroot/css/custom.css | 7 +++++++ 4 files changed, 20 insertions(+), 10 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 433ff5e..f12162f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -3,6 +3,7 @@ * [Changed] Core and Blazor Frontend updated to .Net 5.0 * [Changed] File Preview on Import Page now read-only * [Changed] Misc small visual updates and fixes on Import Page +* [Changed] Consistent Chart Header styles on Report Page ### 1.3 (2020-12-15) diff --git a/OpenBudgeteer.Blazor/Pages/Report.razor b/OpenBudgeteer.Blazor/Pages/Report.razor index 9b8f833..3b4a236 100644 --- a/OpenBudgeteer.Blazor/Pages/Report.razor +++ b/OpenBudgeteer.Blazor/Pages/Report.razor @@ -15,23 +15,27 @@
- +

Month Balances

+
- +

Bank Balances

+
- +

Income & Expenses per Month

+
- +

Income & Expenses per Year

+
-

Bucket Monthly Expenses

+

Bucket Monthly Expenses

@foreach (var config in _monthBucketExpensesConfigsLeft) {
@config.Item1
@@ -41,7 +45,7 @@ }
-

Bucket Monthly Expenses

+

Bucket Monthly Expenses

@foreach (var config in _monthBucketExpensesConfigsRight) {
@config.Item1
diff --git a/OpenBudgeteer.Blazor/ViewModels/BlazorReportViewModel.cs b/OpenBudgeteer.Blazor/ViewModels/BlazorReportViewModel.cs index 2c29efe..c97ea59 100644 --- a/OpenBudgeteer.Blazor/ViewModels/BlazorReportViewModel.cs +++ b/OpenBudgeteer.Blazor/ViewModels/BlazorReportViewModel.cs @@ -63,8 +63,7 @@ public ObservableCollection> MonthBucketExpensesConfigs { Title = new OptionsTitle { - Display = true, - FontSize = 25 + Display = false }, Animation = new ArcAnimation { @@ -107,8 +106,7 @@ protected BarConfig DefaultBucketExpensesBarConfig { Title = new OptionsTitle { - Display = true, - FontSize = 25 + Display = false }, Legend = new Legend { diff --git a/OpenBudgeteer.Blazor/wwwroot/css/custom.css b/OpenBudgeteer.Blazor/wwwroot/css/custom.css index f35d5aa..9db4287 100644 --- a/OpenBudgeteer.Blazor/wwwroot/css/custom.css +++ b/OpenBudgeteer.Blazor/wwwroot/css/custom.css @@ -18,6 +18,13 @@ table .header-row td, table .header-row th { .col-buttons { text-align: right; } +.report-chart-header { + color: gray; + font-weight: bold; + margin: 10px; + text-align: center +} + /* Overwrite site styles*/ html, body { From b0521672a7adf94c80132e7bbcce1305e0850405 Mon Sep 17 00:00:00 2001 From: Axelander Date: Tue, 22 Dec 2020 14:42:55 +0100 Subject: [PATCH 10/50] Style refactoring on Report Page --- OpenBudgeteer.Blazor/Pages/Report.razor | 12 ++++++------ OpenBudgeteer.Blazor/wwwroot/css/custom.css | 9 ++++++++- 2 files changed, 14 insertions(+), 7 deletions(-) diff --git a/OpenBudgeteer.Blazor/Pages/Report.razor b/OpenBudgeteer.Blazor/Pages/Report.razor index 3b4a236..8943d7e 100644 --- a/OpenBudgeteer.Blazor/Pages/Report.razor +++ b/OpenBudgeteer.Blazor/Pages/Report.razor @@ -14,27 +14,27 @@ @inject DbContextOptions DbContextOptions
-
+

Month Balances

-
+

Bank Balances

-
+

Income & Expenses per Month

-
+

Income & Expenses per Year

-
+

Bucket Monthly Expenses

@foreach (var config in _monthBucketExpensesConfigsLeft) { @@ -44,7 +44,7 @@
}
-
+

Bucket Monthly Expenses

@foreach (var config in _monthBucketExpensesConfigsRight) { diff --git a/OpenBudgeteer.Blazor/wwwroot/css/custom.css b/OpenBudgeteer.Blazor/wwwroot/css/custom.css index 9db4287..c34cbcd 100644 --- a/OpenBudgeteer.Blazor/wwwroot/css/custom.css +++ b/OpenBudgeteer.Blazor/wwwroot/css/custom.css @@ -18,11 +18,18 @@ table .header-row td, table .header-row th { .col-buttons { text-align: right; } +.report-chart-box { + width: 900px; + border-width: thick; + border-color: gray; + margin: 5px; + border-style: solid; +} .report-chart-header { color: gray; font-weight: bold; margin: 10px; - text-align: center + text-align: center; } From a5128403909d4e9e196dafc19b4d2a0d35c13d0e Mon Sep 17 00:00:00 2001 From: Axelander Date: Tue, 22 Dec 2020 14:48:32 +0100 Subject: [PATCH 11/50] Code cleanup on Report Page --- OpenBudgeteer.Blazor/Pages/Report.razor | 28 +------------------------ 1 file changed, 1 insertion(+), 27 deletions(-) diff --git a/OpenBudgeteer.Blazor/Pages/Report.razor b/OpenBudgeteer.Blazor/Pages/Report.razor index 8943d7e..13540ea 100644 --- a/OpenBudgeteer.Blazor/Pages/Report.razor +++ b/OpenBudgeteer.Blazor/Pages/Report.razor @@ -2,12 +2,7 @@ @using ChartJs.Blazor @using ChartJs.Blazor.ChartJS.BarChart -@using ChartJs.Blazor.ChartJS.Common.Enums -@using ChartJs.Blazor.ChartJS.Common.Handlers @using ChartJs.Blazor.Charts -@using ChartJs.Blazor.ChartJS.PieChart -@using ChartJs.Blazor.ChartJS.Common.Properties -@using ChartJs.Blazor.Util @using Microsoft.EntityFrameworkCore @using OpenBudgeteer.Core.Common.Database @using OpenBudgeteer.Blazor.ViewModels @@ -55,27 +50,6 @@ }
-@*
-
- - - - - - - - - @foreach (var config in _dataContext.MonthlyBucketExpensesConfigs) - { - - - - - } - -
BucketMonthly Expenses
@config.Item1
-
-
*@ @code { BlazorReportViewModel _dataContext; @@ -90,7 +64,7 @@ _dataContext = new BlazorReportViewModel(DbContextOptions); await _dataContext.LoadDataAsync(); - int halfIndex = _dataContext.MonthBucketExpensesConfigs.Count / 2; + var halfIndex = _dataContext.MonthBucketExpensesConfigs.Count / 2; _monthBucketExpensesConfigsLeft.AddRange(_dataContext.MonthBucketExpensesConfigs.ToList().GetRange(0,halfIndex)); _monthBucketExpensesConfigsRight.AddRange(_dataContext.MonthBucketExpensesConfigs.ToList().GetRange(halfIndex,_dataContext.MonthBucketExpensesConfigs.Count - halfIndex)); } From a9d76710eb0006ef9ea8d9efd00088391aa333fe Mon Sep 17 00:00:00 2001 From: Axelander Date: Thu, 24 Jun 2021 09:19:43 +0200 Subject: [PATCH 12/50] Reworked UI update handling to fix issues on refreshing data #22 --- CHANGELOG.md | 1 + OpenBudgeteer.Blazor/Pages/Account.razor | 15 ++-- OpenBudgeteer.Blazor/Pages/Bucket.razor | 52 ++++++------- OpenBudgeteer.Blazor/Pages/Rules.razor | 28 +++---- OpenBudgeteer.Blazor/Pages/Transaction.razor | 35 +++++---- OpenBudgeteer.Blazor/appsettings.json | 2 +- .../BucketViewModelIsolatedTest.cs | 2 +- .../Common/ViewModelOperationResult.cs | 6 +- .../ViewModels/AccountViewModel.cs | 74 ++++++++----------- .../ViewModels/BucketViewModel.cs | 41 ++++------ .../ItemViewModels/AccountViewModelItem.cs | 8 -- .../BucketGroupViewModelItem.cs | 11 --- .../ItemViewModels/BucketViewModelItem.cs | 25 +------ .../ItemViewModels/RuleSetViewModelItem.cs | 4 + .../TransactionViewModelItem.cs | 12 +-- .../ViewModels/RulesViewModel.cs | 15 ---- .../ViewModels/TransactionViewModel.cs | 21 ------ 17 files changed, 125 insertions(+), 227 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index f12162f..6ada154 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,7 @@ * [Changed] File Preview on Import Page now read-only * [Changed] Misc small visual updates and fixes on Import Page * [Changed] Consistent Chart Header styles on Report Page +* [Fixed] Reworked UI update handling to fix issues on refreshing data #22 ### 1.3 (2020-12-15) diff --git a/OpenBudgeteer.Blazor/Pages/Account.razor b/OpenBudgeteer.Blazor/Pages/Account.razor index b26d108..a70b367 100644 --- a/OpenBudgeteer.Blazor/Pages/Account.razor +++ b/OpenBudgeteer.Blazor/Pages/Account.razor @@ -69,13 +69,7 @@ protected override void OnInitialized() { _dataContext = new AccountViewModel(DbContextOptions); - _dataContext.LoadData(); - _dataContext.ViewModelReloadRequired += (sender, args) => - { - _dataContext.LoadData(); - StateHasChanged(); - }; - HandleResult(new ViewModelOperationResult(true)); + HandleResult(_dataContext.LoadData()); } private void CreateNewAccount() @@ -102,7 +96,7 @@ private void CancelChanges() { _isEditAccountModalDialogVisible = false; - _dataContext.CancelEditMode(); + HandleResult(_dataContext.LoadData()); } private void CloseAccount(AccountViewModelItem account) @@ -117,6 +111,11 @@ _errorModalDialogMessage = result.Message; _isErrorModalDialogVisible = true; } + if (result.ViewModelReloadRequired) + { + _dataContext.LoadData(); + StateHasChanged(); + } } async void DisplayAccountTransactions(AccountViewModelItem account) diff --git a/OpenBudgeteer.Blazor/Pages/Bucket.razor b/OpenBudgeteer.Blazor/Pages/Bucket.razor index 24967f4..b47323a 100644 --- a/OpenBudgeteer.Blazor/Pages/Bucket.razor +++ b/OpenBudgeteer.Blazor/Pages/Bucket.razor @@ -302,8 +302,8 @@ Title="Edit Bucket" DataContext="@_editBucketDialogDataContext" IsDialogVisible="@_isEditBucketModalDialogVisible" - OnCancelClickCallback="@(() => CancelEditBucketDialog())" - OnSaveClickCallback="@(() => SaveAndCloseEditBucketDialog())"/> + OnCancelClickCallback="@(CancelEditBucketDialog)" + OnSaveClickCallback="@(SaveAndCloseEditBucketDialog)"/> - { - HandleResult(await _dataContext.LoadDataAsync()); - StateHasChanged(); - }; - + await HandleResult(await _dataContext.LoadDataAsync()); + YearMonthDataContext.SelectedYearMonthChanged += async (sender, args) => { - HandleResult(await _dataContext.LoadDataAsync()); + await HandleResult(await _dataContext.LoadDataAsync()); StateHasChanged(); }; } - void CreateGroup() + async void CreateGroup() { - HandleResult(_dataContext.CreateGroup()); + await HandleResult(_dataContext.CreateGroup()); } - void DistributeBudget() + async void DistributeBudget() { - HandleResult(_dataContext.DistributeBudget()); + await HandleResult(_dataContext.DistributeBudget()); } void CreateBucket(BucketGroupViewModelItem bucketGroup) @@ -373,51 +368,56 @@ _isEditBucketModalDialogVisible = true; } - void SaveAndCloseEditBucketDialog() + async void SaveAndCloseEditBucketDialog() { _isEditBucketModalDialogVisible = false; - SaveChanges(_editBucketDialogDataContext); + await SaveChanges(_editBucketDialogDataContext); } async void CancelEditBucketDialog() { _isEditBucketModalDialogVisible = false; - HandleResult(await _dataContext.LoadDataAsync()); + await HandleResult(await _dataContext.LoadDataAsync()); StateHasChanged(); } - void SaveChanges(BucketViewModelItem bucket) + async Task SaveChanges(BucketViewModelItem bucket) { - HandleResult(_dataContext.SaveChanges(bucket)); + await HandleResult(_dataContext.SaveChanges(bucket)); StateHasChanged(); } - void CloseBucket(BucketViewModelItem bucket) + async Task CloseBucket(BucketViewModelItem bucket) { - HandleResult(_dataContext.CloseBucket(bucket)); + await HandleResult(_dataContext.CloseBucket(bucket)); StateHasChanged(); } - void HandleResult(ViewModelOperationResult result) + async Task HandleResult(ViewModelOperationResult result) { if (!result.IsSuccessful) { _errorModalDialogMessage = result.Message; _isErrorModalDialogVisible = true; + } + if (result.ViewModelReloadRequired) + { + await _dataContext.LoadDataAsync(); + StateHasChanged(); } } - void InOut_Changed(BucketViewModelItem bucket, KeyboardEventArgs args) + async void InOut_Changed(BucketViewModelItem bucket, KeyboardEventArgs args) { var result = bucket.HandleInOutInput(args.Key); if (result.IsSuccessful) { - HandleResult(_dataContext.UpdateBalanceFigures()); + await HandleResult(_dataContext.UpdateBalanceFigures()); StateHasChanged(); } else { - HandleResult(result); + await HandleResult(result); } } @@ -428,7 +428,7 @@ _transactionModalDialogDataContext = new TransactionViewModel(DbContextOptions, YearMonthDataContext); _transactionModalDialogBucket = bucket.Bucket; - HandleResult(await _transactionModalDialogDataContext.LoadDataAsync(bucket.Bucket, true)); + await HandleResult(await _transactionModalDialogDataContext.LoadDataAsync(bucket.Bucket, true)); _isTransactionModalDialogDataContextLoading = false; StateHasChanged(); diff --git a/OpenBudgeteer.Blazor/Pages/Rules.razor b/OpenBudgeteer.Blazor/Pages/Rules.razor index 1d9febd..f60c0ea 100644 --- a/OpenBudgeteer.Blazor/Pages/Rules.razor +++ b/OpenBudgeteer.Blazor/Pages/Rules.razor @@ -190,7 +190,7 @@ } - + @@ -250,12 +250,7 @@ { _dataContext = new RulesViewModel(DbContextOptions); - HandleResult(await _dataContext.LoadDataAsync()); - _dataContext.ViewModelReloadRequired += async (sender, args) => - { - HandleResult(await _dataContext.LoadDataAsync()); - base.StateHasChanged(); - }; + await HandleResult(await _dataContext.LoadDataAsync()); } void HandleTargetBucketSelectionChanged(RuleSetViewModelItem ruleSetViewModelItem, int selectedBucketId) @@ -276,16 +271,16 @@ _dataContext.EditAllRules(); } - void SaveAllRules() + async void SaveAllRules() { _massEditEnabled = false; - HandleResult(_dataContext.SaveAllRules()); + await HandleResult(_dataContext.SaveAllRules()); } - void CancelAllRules() + async void CancelAllRules() { _massEditEnabled = false; - _dataContext.CancelAllRules(); + await HandleResult(await _dataContext.LoadDataAsync()); } void HandleRuleSetDeletionRequest(RuleSetViewModelItem ruleSet) @@ -300,18 +295,23 @@ _ruleSetToBeDeleted = null; } - void DeleteRule() + async void DeleteRule() { _isDeleteRuleSetModalDialogVisible = false; - HandleResult(_dataContext.DeleteRuleSetItem(_ruleSetToBeDeleted)); + await HandleResult(_dataContext.DeleteRuleSetItem(_ruleSetToBeDeleted)); } - void HandleResult(ViewModelOperationResult result) + async Task HandleResult(ViewModelOperationResult result) { if (!result.IsSuccessful) { _errorModalDialogMessage = result.Message; _isErrorModalDialogVisible = true; + } + if (result.ViewModelReloadRequired) + { + await _dataContext.LoadDataAsync(); + StateHasChanged(); } } } diff --git a/OpenBudgeteer.Blazor/Pages/Transaction.razor b/OpenBudgeteer.Blazor/Pages/Transaction.razor index a14e089..d1d54dd 100644 --- a/OpenBudgeteer.Blazor/Pages/Transaction.razor +++ b/OpenBudgeteer.Blazor/Pages/Transaction.razor @@ -168,7 +168,7 @@ - + @@ -231,16 +231,11 @@ { _dataContext = new TransactionViewModel(DbContextOptions, YearMonthDataContext); - HandleResult(await _dataContext.LoadDataAsync()); - _dataContext.ViewModelReloadRequired += async (sender, args) => - { - HandleResult(await _dataContext.LoadDataAsync()); - base.StateHasChanged(); - }; + await HandleResult(await _dataContext.LoadDataAsync()); YearMonthDataContext.SelectedYearMonthChanged += async (sender, args) => { - HandleResult(await _dataContext.LoadDataAsync()); + await HandleResult(await _dataContext.LoadDataAsync()); base.StateHasChanged(); }; } @@ -263,16 +258,21 @@ if (_dataContext.Transactions.Any(i => i.InModification)) _massEditEnabled = true; } - void SaveAllTransaction() + async void SaveAllTransaction() { _massEditEnabled = false; - HandleResult(_dataContext.SaveAllTransaction()); + await HandleResult(_dataContext.SaveAllTransaction()); } - void CancelAllTransaction() + async void CancelAllTransaction() { _massEditEnabled = false; - _dataContext.CancelAllTransaction(); + await HandleResult(await _dataContext.LoadDataAsync()); + } + + async void SaveTransaction(TransactionViewModelItem transaction) + { + await HandleResult(transaction.UpdateItem()); } void HandleTransactionDeletionRequest(TransactionViewModelItem transaction) @@ -287,18 +287,23 @@ _transactionToBeDeleted = null; } - void DeleteTransaction() + async void DeleteTransaction() { _isDeleteTransactionModalDialogVisible = false; - HandleResult(_transactionToBeDeleted.DeleteItem()); + await HandleResult(_transactionToBeDeleted.DeleteItem()); } - void HandleResult(ViewModelOperationResult result) + async Task HandleResult(ViewModelOperationResult result) { if (!result.IsSuccessful) { _errorModalDialogMessage = result.Message; _isErrorModalDialogVisible = true; + } + if (result.ViewModelReloadRequired) + { + await _dataContext.LoadDataAsync(); + StateHasChanged(); } } } \ No newline at end of file diff --git a/OpenBudgeteer.Blazor/appsettings.json b/OpenBudgeteer.Blazor/appsettings.json index 1c937d5..7e2a179 100644 --- a/OpenBudgeteer.Blazor/appsettings.json +++ b/OpenBudgeteer.Blazor/appsettings.json @@ -2,7 +2,7 @@ "Connection": { "Provider" : "mysql", "Database": "openbudgeteer-dev", - "Server": "cl4p-tp", + "Server": "192.168.178.93", "Port": "3306", "User": "openbudgeteer-dev", "Password": "openbudgeteer-dev" diff --git a/OpenBudgeteer.Core.Test/ViewModelTest/BucketViewModelIsolatedTest.cs b/OpenBudgeteer.Core.Test/ViewModelTest/BucketViewModelIsolatedTest.cs index 3b7c72e..9e4f2bb 100644 --- a/OpenBudgeteer.Core.Test/ViewModelTest/BucketViewModelIsolatedTest.cs +++ b/OpenBudgeteer.Core.Test/ViewModelTest/BucketViewModelIsolatedTest.cs @@ -119,7 +119,7 @@ public async Task DeleteGroup_CheckGroupDeletionAndPositions() var result = viewModel.DeleteGroup(groupToDelete); Assert.True(result.IsSuccessful); - Assert.True(result.ViewModelReloadInvoked); + Assert.True(result.ViewModelReloadRequired); await viewModel.LoadDataAsync(); diff --git a/OpenBudgeteer.Core/Common/ViewModelOperationResult.cs b/OpenBudgeteer.Core/Common/ViewModelOperationResult.cs index 1683284..af08105 100644 --- a/OpenBudgeteer.Core/Common/ViewModelOperationResult.cs +++ b/OpenBudgeteer.Core/Common/ViewModelOperationResult.cs @@ -8,13 +8,13 @@ public class ViewModelOperationResult { public bool IsSuccessful { get; } public string Message { get; } - public bool ViewModelReloadInvoked { get; } + public bool ViewModelReloadRequired { get; } - public ViewModelOperationResult(bool isSuccessful, string message, bool viewModelReloadInvoked = false) + public ViewModelOperationResult(bool isSuccessful, string message, bool viewModelReloadRequired = false) { IsSuccessful = isSuccessful; Message = message; - ViewModelReloadInvoked = viewModelReloadInvoked; + ViewModelReloadRequired = viewModelReloadRequired; } public ViewModelOperationResult(bool isSuccessful, bool viewModelReloadInvoked = false) diff --git a/OpenBudgeteer.Core/ViewModels/AccountViewModel.cs b/OpenBudgeteer.Core/ViewModels/AccountViewModel.cs index 9704d65..59a94fd 100644 --- a/OpenBudgeteer.Core/ViewModels/AccountViewModel.cs +++ b/OpenBudgeteer.Core/ViewModels/AccountViewModel.cs @@ -6,6 +6,7 @@ using System.Text; using System.Windows; using Microsoft.EntityFrameworkCore; +using OpenBudgeteer.Core.Common; using OpenBudgeteer.Core.Common.Database; using OpenBudgeteer.Core.Common.EventClasses; using OpenBudgeteer.Core.Models; @@ -25,12 +26,6 @@ public ObservableCollection Accounts set => Set(ref _accounts, value); } - /// - /// EventHandler which should be invoked in case the whole ViewModel has to be reloaded - /// e.g. due to various database record changes - /// - public event EventHandler ViewModelReloadRequired; - private readonly DbContextOptions _dbOptions; /// @@ -46,43 +41,48 @@ public AccountViewModel(DbContextOptions dbOptions) /// /// Initialize ViewModel and load data from database /// - public void LoadData() + public ViewModelOperationResult LoadData() { - Accounts.Clear(); - - using (var accountDbContext = new DatabaseContext(_dbOptions)) + try { - foreach (var account in accountDbContext.Account - .Where(i => i.IsActive == 1) - .OrderBy(i => i.Name)) + Accounts.Clear(); + using (var accountDbContext = new DatabaseContext(_dbOptions)) { - var newAccountItem = new AccountViewModelItem(_dbOptions, account); - decimal newIn = 0; - decimal newOut = 0; - - using (var transactionDbContext = new DatabaseContext(_dbOptions)) + foreach (var account in accountDbContext.Account + .Where(i => i.IsActive == 1) + .OrderBy(i => i.Name)) { - var transactions = transactionDbContext.BankTransaction - .Where(i => i.AccountId == account.AccountId); + var newAccountItem = new AccountViewModelItem(_dbOptions, account); + decimal newIn = 0; + decimal newOut = 0; - foreach (var transaction in transactions) + using (var transactionDbContext = new DatabaseContext(_dbOptions)) { - if (transaction.Amount > 0) - newIn += transaction.Amount; - else - newOut += transaction.Amount; + var transactions = transactionDbContext.BankTransaction + .Where(i => i.AccountId == account.AccountId); + + foreach (var transaction in transactions) + { + if (transaction.Amount > 0) + newIn += transaction.Amount; + else + newOut += transaction.Amount; + } } - } - newAccountItem.Balance = newIn + newOut; - newAccountItem.In = newIn; - newAccountItem.Out = newOut; + newAccountItem.Balance = newIn + newOut; + newAccountItem.In = newIn; + newAccountItem.Out = newOut; - newAccountItem.ViewModelReloadRequired += (sender, args) => - ViewModelReloadRequired?.Invoke(this, new ViewModelReloadEventArgs(args.ViewModel)); - Accounts.Add(newAccountItem); + Accounts.Add(newAccountItem); + } } } + catch (Exception e) + { + return new ViewModelOperationResult(false, $"Error during loading: {e.Message}"); + } + return new ViewModelOperationResult(true); } /// @@ -98,17 +98,7 @@ public AccountViewModelItem PrepareNewAccount() In = 0, Out = 0 }; - result.ViewModelReloadRequired += (sender, args) => - ViewModelReloadRequired?.Invoke(this, new ViewModelReloadEventArgs(args.ViewModel)); return result; } - - /// - /// Forces reload of ViewModel to revoke unsaved changes - /// - public void CancelEditMode() - { - ViewModelReloadRequired?.Invoke(this, new ViewModelReloadEventArgs(this)); - } } } diff --git a/OpenBudgeteer.Core/ViewModels/BucketViewModel.cs b/OpenBudgeteer.Core/ViewModels/BucketViewModel.cs index 041cc1a..789be33 100644 --- a/OpenBudgeteer.Core/ViewModels/BucketViewModel.cs +++ b/OpenBudgeteer.Core/ViewModels/BucketViewModel.cs @@ -109,12 +109,6 @@ public ObservableCollection BucketGroups private set => Set(ref _bucketGroups, value); } - /// - /// EventHandler which should be invoked in case the whole ViewModel has to be reloaded - /// e.g. due to various database record changes - /// - public event EventHandler ViewModelReloadRequired; - private readonly DbContextOptions _dbOptions; private readonly YearMonthSelectorViewModel _yearMonthViewModel; @@ -152,10 +146,6 @@ public async Task LoadDataAsync() { var newBucketGroup = new BucketGroupViewModelItem(_dbOptions, bucketGroup, _yearMonthViewModel.CurrentMonth); newBucketGroup.IsCollapsed = _defaultCollapseState; - newBucketGroup.ViewModelReloadRequired += (sender, args) => - { - ViewModelReloadRequired?.Invoke(this, new ViewModelReloadEventArgs(args.ViewModel)); - }; var buckets = dbContext.Bucket .Where(i => i.BucketGroupId == newBucketGroup.BucketGroup.BucketGroupId) .OrderBy(i => i.Name) @@ -172,10 +162,6 @@ public async Task LoadDataAsync() foreach (var bucket in await Task.WhenAll(bucketItemTasks)) { - bucket.ViewModelReloadRequired += (sender, args) => - { - ViewModelReloadRequired?.Invoke(this, new ViewModelReloadEventArgs(args.ViewModel)); - }; newBucketGroup.Buckets.Add(bucket); } BucketGroups.Add(newBucketGroup); @@ -218,12 +204,7 @@ public ViewModelOperationResult CreateGroup() new BucketGroupViewModelItem(_dbOptions, newGroup, _yearMonthViewModel.CurrentMonth) { InModification = true - }; - newBucketGroupViewModelItem.ViewModelReloadRequired += (sender, args) => - { - ViewModelReloadRequired?.Invoke(this, new ViewModelReloadEventArgs(args.ViewModel)); - }; BucketGroups.Insert(0, newBucketGroupViewModelItem); return new ViewModelOperationResult(true); } @@ -259,7 +240,6 @@ public ViewModelOperationResult DeleteGroup(BucketGroupViewModelItem bucketGroup dbContext.UpdateBucketGroups(dbBucketGroups); transaction.Commit(); - ViewModelReloadRequired?.Invoke(this, new ViewModelReloadEventArgs(this)); return new ViewModelOperationResult(true, true); } catch (Exception e) @@ -300,7 +280,6 @@ public ViewModelOperationResult DistributeBudget() transaction.Commit(); //UpdateBalanceFigures(); // Should be done but not required because it will be done during ViewModel reload - ViewModelReloadRequired?.Invoke(this, new ViewModelReloadEventArgs(this)); return new ViewModelOperationResult(true, true); } catch (Exception e) @@ -374,7 +353,7 @@ public ViewModelOperationResult UpdateBalanceFigures() } /// - /// Helper methode to set Collapse status for all + /// Helper method to set Collapse status for all /// /// New collapse status public void ChangeBucketGroupCollapse(bool collapse = true) @@ -394,9 +373,12 @@ public void ChangeBucketGroupCollapse(bool collapse = true) /// Object which contains information and results of this method public ViewModelOperationResult SaveChanges(BucketViewModelItem bucket) { - var result = bucket.CreateOrUpdateBucket(); - if (!result.IsSuccessful) return result; - return UpdateBalanceFigures(); + var createUpdateResult = bucket.CreateOrUpdateBucket(); + if (!createUpdateResult.IsSuccessful) return createUpdateResult; + var updateFiguresResult = UpdateBalanceFigures(); + return new ViewModelOperationResult( + updateFiguresResult.IsSuccessful, + createUpdateResult.ViewModelReloadRequired || updateFiguresResult.ViewModelReloadRequired); } /// @@ -408,9 +390,12 @@ public ViewModelOperationResult SaveChanges(BucketViewModelItem bucket) /// Object which contains information and results of this method public ViewModelOperationResult CloseBucket(BucketViewModelItem bucket) { - var result = bucket.CloseBucket(); - if (!result.IsSuccessful) return result; - return UpdateBalanceFigures(); + var closeBucketResult = bucket.CloseBucket(); + if (!closeBucketResult.IsSuccessful) return closeBucketResult; + var updateFiguresResult = UpdateBalanceFigures(); + return new ViewModelOperationResult( + updateFiguresResult.IsSuccessful, + closeBucketResult.ViewModelReloadRequired || updateFiguresResult.ViewModelReloadRequired); } } } diff --git a/OpenBudgeteer.Core/ViewModels/ItemViewModels/AccountViewModelItem.cs b/OpenBudgeteer.Core/ViewModels/ItemViewModels/AccountViewModelItem.cs index a22de03..4caf34e 100644 --- a/OpenBudgeteer.Core/ViewModels/ItemViewModels/AccountViewModelItem.cs +++ b/OpenBudgeteer.Core/ViewModels/ItemViewModels/AccountViewModelItem.cs @@ -51,12 +51,6 @@ public decimal Out set => Set(ref _out, value); } - /// - /// EventHandler which should be invoked in case the whole ViewModel has to be reloaded - /// e.g. due to various database record changes - /// - public event EventHandler ViewModelReloadRequired; - private readonly DbContextOptions _dbOptions; /// @@ -89,7 +83,6 @@ public ViewModelOperationResult CreateUpdateAccount() { var result = Account.AccountId == 0 ? dbContext.CreateAccount(Account) : dbContext.UpdateAccount(Account); if (result == 0) return new ViewModelOperationResult(false, "Unable to save changes to database"); - ViewModelReloadRequired?.Invoke(this, new ViewModelReloadEventArgs(this)); return new ViewModelOperationResult(true, true); } } @@ -108,7 +101,6 @@ public ViewModelOperationResult CloseAccount() { var result = dbContext.UpdateAccount(Account); if (result == 0) return new ViewModelOperationResult(false, "Unable to save changes to database"); - ViewModelReloadRequired?.Invoke(this, new ViewModelReloadEventArgs(this)); return new ViewModelOperationResult(true, true); } } diff --git a/OpenBudgeteer.Core/ViewModels/ItemViewModels/BucketGroupViewModelItem.cs b/OpenBudgeteer.Core/ViewModels/ItemViewModels/BucketGroupViewModelItem.cs index d09db17..030b18f 100644 --- a/OpenBudgeteer.Core/ViewModels/ItemViewModels/BucketGroupViewModelItem.cs +++ b/OpenBudgeteer.Core/ViewModels/ItemViewModels/BucketGroupViewModelItem.cs @@ -73,12 +73,6 @@ public bool InModification set => Set(ref _inModification, value); } - /// - /// EventHandler which should be invoked in case the whole ViewModel has to be reloaded - /// e.g. due to various database record changes - /// - public event EventHandler ViewModelReloadRequired; - private readonly DateTime _currentMonth; private readonly DbContextOptions _dbOptions; private BucketGroup _oldBucketGroup; @@ -145,7 +139,6 @@ public ViewModelOperationResult SaveModification() } InModification = false; _oldBucketGroup = null; - ViewModelReloadRequired?.Invoke(this, new ViewModelReloadEventArgs(this)); return new ViewModelOperationResult(true, true); } catch (Exception e) @@ -193,7 +186,6 @@ public ViewModelOperationResult MoveGroup(int positions) } transaction.Commit(); - ViewModelReloadRequired?.Invoke(this, new ViewModelReloadEventArgs(this)); return new ViewModelOperationResult(true, true); } catch (Exception e) @@ -208,9 +200,6 @@ public ViewModelOperationResult MoveGroup(int positions) public BucketViewModelItem CreateBucket() { var newBucket = new BucketViewModelItem(_dbOptions, BucketGroup, _currentMonth); - // Hand over ViewModel changes - newBucket.ViewModelReloadRequired += (sender, args) => - ViewModelReloadRequired?.Invoke(this, new ViewModelReloadEventArgs(this)); Buckets.Add(newBucket); return newBucket; } diff --git a/OpenBudgeteer.Core/ViewModels/ItemViewModels/BucketViewModelItem.cs b/OpenBudgeteer.Core/ViewModels/ItemViewModels/BucketViewModelItem.cs index 940b06a..84f2137 100644 --- a/OpenBudgeteer.Core/ViewModels/ItemViewModels/BucketViewModelItem.cs +++ b/OpenBudgeteer.Core/ViewModels/ItemViewModels/BucketViewModelItem.cs @@ -166,12 +166,6 @@ public ObservableCollection AvailableBucketGroups set => Set(ref _availableBucketGroups, value); } - /// - /// EventHandler which should be invoked in case the whole ViewModel has to be reloaded - /// e.g. due to various database record changes - /// - public event EventHandler ViewModelReloadRequired; - private readonly bool _isNewlyCreatedBucket; private readonly DateTime _currentYearMonth; private readonly DbContextOptions _dbOptions; @@ -503,7 +497,6 @@ public ViewModelOperationResult CloseBucket() } } } - ViewModelReloadRequired?.Invoke(this, new ViewModelReloadEventArgs(this)); return new ViewModelOperationResult(true, true); } @@ -519,7 +512,7 @@ public ViewModelOperationResult CloseBucket() public ViewModelOperationResult CreateOrUpdateBucket() { var result = _isNewlyCreatedBucket ? CreateBucket() : UpdateBucket(); - if (!result.IsSuccessful || result.ViewModelReloadInvoked) return result; + if (!result.IsSuccessful || result.ViewModelReloadRequired) return result; InModification = false; CalculateValues(); return new ViewModelOperationResult(true); @@ -552,13 +545,11 @@ private ViewModelOperationResult CreateBucket() $"Bucket ID: {newBucketVersion.BucketId}"); transaction.Commit(); - ViewModelReloadRequired?.Invoke(this, new ViewModelReloadEventArgs(this)); return new ViewModelOperationResult(true, true); } catch (Exception e) { transaction.Rollback(); - ViewModelReloadRequired?.Invoke(this, new ViewModelReloadEventArgs(this)); return new ViewModelOperationResult( false, $"Error during database update: {e.Message}", @@ -576,7 +567,6 @@ private ViewModelOperationResult CreateBucket() /// Object which contains information and results of this method private ViewModelOperationResult UpdateBucket() { - var forceViewModelReload = false; using (var dbContext = new DatabaseContext(_dbOptions)) { using (var transaction = dbContext.Database.BeginTransaction()) @@ -591,7 +581,7 @@ private ViewModelOperationResult UpdateBucket() { // BucketGroup update requires special handling as ViewModel needs to trigger reload // to force re-rendering of Blazor Page - if (dbBucket.BucketGroupId != Bucket.BucketGroupId) forceViewModelReload = true; + //if (dbBucket.BucketGroupId != Bucket.BucketGroupId) forceViewModelReload = true; if (dbContext.UpdateBucket(Bucket) == 0) throw new Exception($"Error during database update: Unable to update Bucket.{Environment.NewLine}" + @@ -638,13 +628,11 @@ private ViewModelOperationResult UpdateBucket() } } transaction.Commit(); - if (forceViewModelReload) ViewModelReloadRequired?.Invoke(this, new ViewModelReloadEventArgs(this)); return new ViewModelOperationResult(true, true); } catch (Exception e) { transaction.Rollback(); - ViewModelReloadRequired?.Invoke(this, new ViewModelReloadEventArgs(this)); return new ViewModelOperationResult( false, $"Error during database update: {e.Message}", @@ -654,15 +642,6 @@ private ViewModelOperationResult UpdateBucket() } } - /// - /// Triggers to cancel all modifications - /// - public void CancelChanges() - { - InModification = false; - ViewModelReloadRequired?.Invoke(this, new ViewModelReloadEventArgs(this)); // force Re-load to get old values back - } - /// /// Helper method to create a new record in the database based on User input /// diff --git a/OpenBudgeteer.Core/ViewModels/ItemViewModels/RuleSetViewModelItem.cs b/OpenBudgeteer.Core/ViewModels/ItemViewModels/RuleSetViewModelItem.cs index a920d93..a5773ce 100644 --- a/OpenBudgeteer.Core/ViewModels/ItemViewModels/RuleSetViewModelItem.cs +++ b/OpenBudgeteer.Core/ViewModels/ItemViewModels/RuleSetViewModelItem.cs @@ -199,6 +199,10 @@ public ViewModelOperationResult CreateUpdateRuleSetItem() } } + foreach (var mappingRuleViewModelItem in MappingRules) + { + mappingRuleViewModelItem.MappingRule.MappingRuleId = 0; + } dbContext.CreateMappingRules(MappingRules.Select(i => i.MappingRule).ToList()); dbTransaction.Commit(); diff --git a/OpenBudgeteer.Core/ViewModels/ItemViewModels/TransactionViewModelItem.cs b/OpenBudgeteer.Core/ViewModels/ItemViewModels/TransactionViewModelItem.cs index 5816f83..b8d705a 100644 --- a/OpenBudgeteer.Core/ViewModels/ItemViewModels/TransactionViewModelItem.cs +++ b/OpenBudgeteer.Core/ViewModels/ItemViewModels/TransactionViewModelItem.cs @@ -74,12 +74,6 @@ public ObservableCollection AvailableAccounts set => Set(ref _availableAccounts, value); } - /// - /// EventHandler which should be invoked in case the whole ViewModel has to be reloaded - /// e.g. due to various database record changes - /// - public event EventHandler ViewModelReloadRequired; - private readonly DbContextOptions _dbOptions; private readonly YearMonthSelectorViewModel _yearMonthViewModel; private TransactionViewModelItem _oldTransactionViewModelItem; @@ -517,22 +511,18 @@ public ViewModelOperationResult UpdateItem() var result = CreateOrUpdateTransaction(); if (!result.IsSuccessful) { - // Trigger page reload as DB Update was not successfully - ViewModelReloadRequired?.Invoke(this, new ViewModelReloadEventArgs(this)); - return new ViewModelOperationResult(false, result.Message, true); } _oldTransactionViewModelItem = null; InModification = false; - return new ViewModelOperationResult(true); + return new ViewModelOperationResult(true, true); } public ViewModelOperationResult DeleteItem() { var result = DeleteTransaction(); if (!result.IsSuccessful) return result; - ViewModelReloadRequired?.Invoke(this, new ViewModelReloadEventArgs(this)); return new ViewModelOperationResult(true, true); } diff --git a/OpenBudgeteer.Core/ViewModels/RulesViewModel.cs b/OpenBudgeteer.Core/ViewModels/RulesViewModel.cs index bdef627..a434291 100644 --- a/OpenBudgeteer.Core/ViewModels/RulesViewModel.cs +++ b/OpenBudgeteer.Core/ViewModels/RulesViewModel.cs @@ -35,12 +35,6 @@ public ObservableCollection RuleSets set => Set(ref _ruleSets, value); } - /// - /// EventHandler which should be invoked in case the whole ViewModel has to be reloaded - /// e.g. due to various database record changes - /// - public event EventHandler ViewModelReloadRequired; - private readonly DbContextOptions _dbOptions; /// @@ -93,7 +87,6 @@ public ViewModelOperationResult CreateNewRuleSet() var result = NewRuleSet.CreateUpdateRuleSetItem(); if (!result.IsSuccessful) return result; ResetNewRuleSet(); - ViewModelReloadRequired?.Invoke(this, new ViewModelReloadEventArgs(this)); return new ViewModelOperationResult(true, true); } @@ -198,13 +191,5 @@ public ViewModelOperationResult SaveAllRules() } } } - - /// - /// Triggers to cancel all changes to all - /// - public void CancelAllRules() - { - ViewModelReloadRequired?.Invoke(this, new ViewModelReloadEventArgs(this)); - } } } diff --git a/OpenBudgeteer.Core/ViewModels/TransactionViewModel.cs b/OpenBudgeteer.Core/ViewModels/TransactionViewModel.cs index d51e5fd..5166b93 100644 --- a/OpenBudgeteer.Core/ViewModels/TransactionViewModel.cs +++ b/OpenBudgeteer.Core/ViewModels/TransactionViewModel.cs @@ -38,12 +38,6 @@ public ObservableCollection Transactions set => Set(ref _transactions, value); } - /// - /// EventHandler which should be invoked in case the whole ViewModel has to be reloaded - /// e.g. due to various database record changes - /// - public event EventHandler ViewModelReloadRequired; - private readonly DbContextOptions _dbOptions; private readonly YearMonthSelectorViewModel _yearMonthViewModel; @@ -89,8 +83,6 @@ public async Task LoadDataAsync() foreach (var transaction in await Task.WhenAll(transactionTasks)) { - transaction.ViewModelReloadRequired += (sender, args) => - ViewModelReloadRequired?.Invoke(this, new ViewModelReloadEventArgs(args.ViewModel)); Transactions.Add(transaction); } @@ -155,8 +147,6 @@ public async Task LoadDataAsync(Bucket bucket, bool wi foreach (var transaction in (await Task.WhenAll(transactionTasks)) .OrderByDescending(i => i.Transaction.TransactionDate)) { - transaction.ViewModelReloadRequired += (sender, args) => - ViewModelReloadRequired?.Invoke(this, new ViewModelReloadEventArgs(args.ViewModel)); Transactions.Add(transaction); } @@ -197,8 +187,6 @@ public async Task LoadDataAsync(Account account) foreach (var transaction in await Task.WhenAll(transactionTasks)) { - transaction.ViewModelReloadRequired += (sender, args) => - ViewModelReloadRequired?.Invoke(this, new ViewModelReloadEventArgs(args.ViewModel)); Transactions.Add(transaction); } @@ -222,7 +210,6 @@ public ViewModelOperationResult CreateItem() var result = NewTransaction.CreateItem(); if (!result.IsSuccessful) return result; ResetNewTransaction(); - ViewModelReloadRequired?.Invoke(this, new ViewModelReloadEventArgs(this)); return new ViewModelOperationResult(true, true); } @@ -273,14 +260,6 @@ public ViewModelOperationResult SaveAllTransaction() } } - /// - /// Triggers to cancel all changes to all Transactions - /// - public void CancelAllTransaction() - { - ViewModelReloadRequired?.Invoke(this, new ViewModelReloadEventArgs(this)); - } - /// /// Starts process to propose the right for all Transactions /// From 1bd45ec8a964382f74be962ada31866a26308abb Mon Sep 17 00:00:00 2001 From: Axelander Date: Thu, 24 Jun 2021 09:48:34 +0200 Subject: [PATCH 13/50] Fixed missing Target Account update for newly created or updated Import Profiles #23 --- OpenBudgeteer.Blazor/Pages/Import.razor | 1 + 1 file changed, 1 insertion(+) diff --git a/OpenBudgeteer.Blazor/Pages/Import.razor b/OpenBudgeteer.Blazor/Pages/Import.razor index 8424401..f5bff8a 100644 --- a/OpenBudgeteer.Blazor/Pages/Import.razor +++ b/OpenBudgeteer.Blazor/Pages/Import.razor @@ -431,6 +431,7 @@ { var value = Convert.ToInt32(e.Value); _dataContext.SelectedAccount = value == _placeholderItemId ? null : _dataContext.AvailableAccounts.First(i => i.AccountId == value); + _dataContext.SelectedImportProfile.AccountId = _dataContext.SelectedAccount?.AccountId ?? 0; } void TransactionDateColumn_SelectionChanged(ChangeEventArgs e) From e73786b3006acf3bac5d3880e3206ce1c5205a0b Mon Sep 17 00:00:00 2001 From: Axelander Date: Thu, 24 Jun 2021 07:50:14 +0000 Subject: [PATCH 14/50] Update CHANGELOG.md --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 6ada154..4990323 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,7 @@ * [Changed] Misc small visual updates and fixes on Import Page * [Changed] Consistent Chart Header styles on Report Page * [Fixed] Reworked UI update handling to fix issues on refreshing data #22 +* [Fixed] Missing Target Account update for newly created or updated Import Profiles #23 ### 1.3 (2020-12-15) From 5a7686018561e4b80d3ad2ba99f7edf0a510618b Mon Sep 17 00:00:00 2001 From: Jonas Van der Aa Date: Sun, 29 Aug 2021 10:34:17 +0000 Subject: [PATCH 15/50] Depedency updates --- CHANGELOG.md | 4 ++ .../OpenBudgeteer.Blazor.csproj | 10 +---- OpenBudgeteer.Blazor/Startup.cs | 1 + .../OpenBudgeteer.Core.Test.csproj | 19 +++++---- .../YearMonthSelectorViewModelTest.cs | 42 ++++++++++++------- .../Common/Database/DatabaseContext.cs | 5 +-- .../Common/Database/MySqlDatabaseContext.cs | 15 ++++--- .../Database/MySqlDatabaseContextFactory.cs | 35 +++++++++++----- .../Common/MonthOutputConverter.cs | 8 ++-- .../MySql/DatabaseServiceModelSnapshot.cs | 16 +++---- OpenBudgeteer.Core/OpenBudgeteer.Core.csproj | 9 ++-- 11 files changed, 95 insertions(+), 69 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 4990323..588e086 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,8 +4,12 @@ * [Changed] File Preview on Import Page now read-only * [Changed] Misc small visual updates and fixes on Import Page * [Changed] Consistent Chart Header styles on Report Page +* [Changed] Updated dependencies +* [Changed] Simplified dependency tree * [Fixed] Reworked UI update handling to fix issues on refreshing data #22 * [Fixed] Missing Target Account update for newly created or updated Import Profiles #23 +* [Fixed] `MonthOutputConverter.Convert` not using Culture +* [Fixed] `OpenBudgeteer.Core.Test.ViewModelTest.YearMonthSelectorViewModelTest.Constructor_CheckDefaults` test using thread culture ### 1.3 (2020-12-15) diff --git a/OpenBudgeteer.Blazor/OpenBudgeteer.Blazor.csproj b/OpenBudgeteer.Blazor/OpenBudgeteer.Blazor.csproj index 5b9aefa..dbfaf3f 100644 --- a/OpenBudgeteer.Blazor/OpenBudgeteer.Blazor.csproj +++ b/OpenBudgeteer.Blazor/OpenBudgeteer.Blazor.csproj @@ -9,14 +9,8 @@ - - - all - runtime; build; native; contentfiles; analyzers; buildtransitive - - - - + + diff --git a/OpenBudgeteer.Blazor/Startup.cs b/OpenBudgeteer.Blazor/Startup.cs index ef2363d..100725f 100644 --- a/OpenBudgeteer.Blazor/Startup.cs +++ b/OpenBudgeteer.Blazor/Startup.cs @@ -49,6 +49,7 @@ public void ConfigureServices(IServiceCollection services) services.AddDbContext(options => options.UseMySql( connectionString, + ServerVersion.AutoDetect(connectionString), b => b.MigrationsAssembly("OpenBudgeteer.Core")), ServiceLifetime.Transient); diff --git a/OpenBudgeteer.Core.Test/OpenBudgeteer.Core.Test.csproj b/OpenBudgeteer.Core.Test/OpenBudgeteer.Core.Test.csproj index a806ce2..d6a03d3 100644 --- a/OpenBudgeteer.Core.Test/OpenBudgeteer.Core.Test.csproj +++ b/OpenBudgeteer.Core.Test/OpenBudgeteer.Core.Test.csproj @@ -10,14 +10,17 @@ - - - - - - - - + + + + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + diff --git a/OpenBudgeteer.Core.Test/ViewModelTest/YearMonthSelectorViewModelTest.cs b/OpenBudgeteer.Core.Test/ViewModelTest/YearMonthSelectorViewModelTest.cs index fa19376..c412bd6 100644 --- a/OpenBudgeteer.Core.Test/ViewModelTest/YearMonthSelectorViewModelTest.cs +++ b/OpenBudgeteer.Core.Test/ViewModelTest/YearMonthSelectorViewModelTest.cs @@ -4,11 +4,19 @@ using OpenBudgeteer.Core.Common; using OpenBudgeteer.Core.ViewModels; using Xunit; +using Xunit.Abstractions; namespace OpenBudgeteer.Core.Test.ViewModelTest { public class YearMonthSelectorViewModelTest { + private readonly ITestOutputHelper _output; + + public YearMonthSelectorViewModelTest(ITestOutputHelper output) + { + _output = output ?? throw new ArgumentNullException(nameof(output)); + } + [Fact] public void Constructor_CheckDefaults() { @@ -24,23 +32,27 @@ public void Constructor_CheckDefaults() { Assert.Equal(i, viewModel.Months.ElementAt(i-1)); } - + var cultureInfo = new CultureInfo("de-DE"); - CultureInfo.DefaultThreadCurrentCulture = cultureInfo; - CultureInfo.DefaultThreadCurrentUICulture = cultureInfo; + + _output.WriteLine($"Current Culture: {CultureInfo.CurrentCulture.Name}"); + _output.WriteLine($"Current UI Culture: {CultureInfo.CurrentUICulture.Name}"); + _output.WriteLine($"Test culture: {cultureInfo.Name}"); + var converter = new MonthOutputConverter(); - Assert.Equal("Jan", converter.ConvertMonth(1)); - Assert.Equal("Feb", converter.ConvertMonth(2)); - Assert.Equal("Mrz", converter.ConvertMonth(3)); - Assert.Equal("Apr", converter.ConvertMonth(4)); - Assert.Equal("Mai", converter.ConvertMonth(5)); - Assert.Equal("Jun", converter.ConvertMonth(6)); - Assert.Equal("Jul", converter.ConvertMonth(7)); - Assert.Equal("Aug", converter.ConvertMonth(8)); - Assert.Equal("Sep", converter.ConvertMonth(9)); - Assert.Equal("Okt", converter.ConvertMonth(10)); - Assert.Equal("Nov", converter.ConvertMonth(11)); - Assert.Equal("Dez", converter.ConvertMonth(12)); + + Assert.Equal("Jan", converter.ConvertMonth(1, cultureInfo)); + Assert.Equal("Feb", converter.ConvertMonth(2, cultureInfo)); + Assert.Equal("Mär", converter.ConvertMonth(3, cultureInfo)); + Assert.Equal("Apr", converter.ConvertMonth(4, cultureInfo)); + Assert.Equal("Mai", converter.ConvertMonth(5, cultureInfo)); + Assert.Equal("Jun", converter.ConvertMonth(6, cultureInfo)); + Assert.Equal("Jul", converter.ConvertMonth(7, cultureInfo)); + Assert.Equal("Aug", converter.ConvertMonth(8, cultureInfo)); + Assert.Equal("Sep", converter.ConvertMonth(9, cultureInfo)); + Assert.Equal("Okt", converter.ConvertMonth(10, cultureInfo)); + Assert.Equal("Nov", converter.ConvertMonth(11, cultureInfo)); + Assert.Equal("Dez", converter.ConvertMonth(12, cultureInfo)); } [Fact] diff --git a/OpenBudgeteer.Core/Common/Database/DatabaseContext.cs b/OpenBudgeteer.Core/Common/Database/DatabaseContext.cs index 1b45fe2..aa8615f 100644 --- a/OpenBudgeteer.Core/Common/Database/DatabaseContext.cs +++ b/OpenBudgeteer.Core/Common/Database/DatabaseContext.cs @@ -1,8 +1,5 @@ -using System; -using System.Collections.Generic; -using System.IO; +using System.Collections.Generic; using System.Linq; -using System.Text; using Microsoft.EntityFrameworkCore; using OpenBudgeteer.Core.Models; diff --git a/OpenBudgeteer.Core/Common/Database/MySqlDatabaseContext.cs b/OpenBudgeteer.Core/Common/Database/MySqlDatabaseContext.cs index d8fe386..9edbe91 100644 --- a/OpenBudgeteer.Core/Common/Database/MySqlDatabaseContext.cs +++ b/OpenBudgeteer.Core/Common/Database/MySqlDatabaseContext.cs @@ -1,15 +1,20 @@ -using System; -using System.Collections.Generic; -using System.Text; -using Microsoft.EntityFrameworkCore; -using Microsoft.EntityFrameworkCore.Design; +using Microsoft.EntityFrameworkCore; namespace OpenBudgeteer.Core.Common.Database { public class MySqlDatabaseContext : DatabaseContext { + private const string CharacterSet = "utf8mb4"; + public MySqlDatabaseContext(DbContextOptions options) : base(options) { } + + protected override void OnModelCreating(ModelBuilder modelBuilder) + { + modelBuilder.HasCharSet(CharacterSet); + + base.OnModelCreating(modelBuilder); + } } } diff --git a/OpenBudgeteer.Core/Common/Database/MySqlDatabaseContextFactory.cs b/OpenBudgeteer.Core/Common/Database/MySqlDatabaseContextFactory.cs index 06cc616..20bdc07 100644 --- a/OpenBudgeteer.Core/Common/Database/MySqlDatabaseContextFactory.cs +++ b/OpenBudgeteer.Core/Common/Database/MySqlDatabaseContextFactory.cs @@ -1,7 +1,4 @@ -using System; -using System.Collections.Generic; -using System.Text; -using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore.Design; using Microsoft.Extensions.Configuration; @@ -9,14 +6,28 @@ namespace OpenBudgeteer.Core.Common.Database { public class MySqlDatabaseContextFactory : IDesignTimeDbContextFactory { + private const string MysqlConnectionString = "Server=192.168.178.30;" + + "Port=3306;" + + "Database=openbudgeteer-dev;" + + "User=openbudgeteer-dev;" + + "Password=openbudgeteer-dev"; + /* + * Pass arguments to args by appending them to the tool call. + * i.e.: dotnet ef migrations add Test -c OpenBudgeteer.Core.Common.Database.MySqlDatabaseContext -- "Server=kitana.vdaa;Port=3307;Database=openbudgeteer-dev;User=openbudgeteer-dev;Password=openbudgeteer-dev" + */ public MySqlDatabaseContext CreateDbContext(string[] args) { - var optionsBuilder = new DbContextOptionsBuilder() - .UseMySql("Server=192.168.178.30;" + - "Port=3306;" + - "Database=openbudgeteer-dev" + - "User=openbudgeteer-dev" + - "Password=openbudgeteer-dev"); + var optionsBuilder = new DbContextOptionsBuilder(); + + if ((args?.Length ?? 0) == 0) + { + optionsBuilder.UseMySql(MysqlConnectionString, ServerVersion.AutoDetect(MysqlConnectionString)); + } + else + { + var connectionString = string.Join(";", args); + optionsBuilder.UseMySql(connectionString, ServerVersion.AutoDetect(connectionString)); + } return new MySqlDatabaseContext(optionsBuilder.Options); } @@ -31,8 +42,10 @@ public MySqlDatabaseContext CreateDbContext(IConfiguration configuration) $"Password={configurationSection?["Password"]}"; var optionsBuilder = new DbContextOptionsBuilder() .UseMySql( - connectionString, + connectionString, + ServerVersion.AutoDetect(connectionString), b => b.MigrationsAssembly("OpenBudgeteer.Core")); + return new MySqlDatabaseContext(optionsBuilder.Options); } } diff --git a/OpenBudgeteer.Core/Common/MonthOutputConverter.cs b/OpenBudgeteer.Core/Common/MonthOutputConverter.cs index 36f9f1b..69fef1e 100644 --- a/OpenBudgeteer.Core/Common/MonthOutputConverter.cs +++ b/OpenBudgeteer.Core/Common/MonthOutputConverter.cs @@ -1,7 +1,5 @@ using System; -using System.Collections.Generic; using System.Globalization; -using System.Text; namespace OpenBudgeteer.Core.Common { @@ -11,7 +9,7 @@ public object Convert(object value, Type targetType, object parameter, CultureIn { if (!(value is int month)) return string.Empty; var date = new DateTime(1, month, 1); - return date.ToString("MMM"); + return date.ToString("MMM", culture); } public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture) @@ -25,9 +23,9 @@ public object ConvertBack(object value, Type targetType, object parameter, Cultu return DateTime.Now.Month; } - public string ConvertMonth(object value) + public string ConvertMonth(object value, CultureInfo culture = null) { - return Convert(value, typeof(string), null, CultureInfo.CurrentCulture).ToString(); + return Convert(value, typeof(string), null, culture ?? CultureInfo.CurrentCulture).ToString(); } } } diff --git a/OpenBudgeteer.Core/Migrations/MySql/DatabaseServiceModelSnapshot.cs b/OpenBudgeteer.Core/Migrations/MySql/DatabaseServiceModelSnapshot.cs index ada9233..7b08ad5 100644 --- a/OpenBudgeteer.Core/Migrations/MySql/DatabaseServiceModelSnapshot.cs +++ b/OpenBudgeteer.Core/Migrations/MySql/DatabaseServiceModelSnapshot.cs @@ -14,8 +14,8 @@ protected override void BuildModel(ModelBuilder modelBuilder) { #pragma warning disable 612, 618 modelBuilder - .HasAnnotation("ProductVersion", "3.1.5") - .HasAnnotation("Relational:MaxIdentifierLength", 64); + .HasAnnotation("Relational:MaxIdentifierLength", 64) + .HasAnnotation("ProductVersion", "3.1.5"); modelBuilder.Entity("OpenBudgeteer.Core.Models.Account", b => { @@ -44,7 +44,7 @@ protected override void BuildModel(ModelBuilder modelBuilder) .HasColumnType("int"); b.Property("Amount") - .HasColumnType("decimal(65, 2)"); + .HasColumnType("decimal(65,2)"); b.Property("Memo") .HasColumnType("longtext CHARACTER SET utf8mb4"); @@ -113,7 +113,7 @@ protected override void BuildModel(ModelBuilder modelBuilder) .HasColumnType("int"); b.Property("Amount") - .HasColumnType("decimal(65, 2)"); + .HasColumnType("decimal(65,2)"); b.Property("BucketId") .HasColumnType("int"); @@ -162,7 +162,7 @@ protected override void BuildModel(ModelBuilder modelBuilder) .HasColumnType("int"); b.Property("BucketTypeYParam") - .HasColumnType("decimal(65, 2)"); + .HasColumnType("decimal(65,2)"); b.Property("BucketTypeZParam") .HasColumnType("datetime(6)"); @@ -188,7 +188,7 @@ protected override void BuildModel(ModelBuilder modelBuilder) .HasColumnType("int"); b.Property("Amount") - .HasColumnType("decimal(65, 2)"); + .HasColumnType("decimal(65,2)"); b.Property("BucketId") .HasColumnType("int"); @@ -218,7 +218,7 @@ protected override void BuildModel(ModelBuilder modelBuilder) b.Property("Delimiter") .IsRequired() - .HasColumnType("varchar(1) CHARACTER SET utf8mb4"); + .HasColumnType("varchar(1)"); b.Property("HeaderRow") .HasColumnType("int"); @@ -237,7 +237,7 @@ protected override void BuildModel(ModelBuilder modelBuilder) b.Property("TextQualifier") .IsRequired() - .HasColumnType("varchar(1) CHARACTER SET utf8mb4"); + .HasColumnType("varchar(1)"); b.Property("TransactionDateColumnName") .HasColumnType("longtext CHARACTER SET utf8mb4"); diff --git a/OpenBudgeteer.Core/OpenBudgeteer.Core.csproj b/OpenBudgeteer.Core/OpenBudgeteer.Core.csproj index f62041e..2ef1342 100644 --- a/OpenBudgeteer.Core/OpenBudgeteer.Core.csproj +++ b/OpenBudgeteer.Core/OpenBudgeteer.Core.csproj @@ -5,14 +5,13 @@ - - - + + all runtime; build; native; contentfiles; analyzers; buildtransitive - - + + From 6ea6dc9c18035eb1d795ec858ed3b3a7644c4e27 Mon Sep 17 00:00:00 2001 From: Axelander Date: Sun, 29 Aug 2021 10:52:28 +0000 Subject: [PATCH 16/50] Add Dialog for showing progress during Bucket proposal --- CHANGELOG.md | 1 + OpenBudgeteer.Blazor/Pages/Transaction.razor | 24 +++++++- .../Shared/ProcessingProgressDialog.razor | 52 ++++++++++++++++++ OpenBudgeteer.Blazor/appsettings.json | 2 +- .../ProposeBucketChangedEventArgs.cs | 20 +++++++ .../ViewModels/TransactionViewModel.cs | 55 ++++++++++++++++--- 6 files changed, 145 insertions(+), 9 deletions(-) create mode 100644 OpenBudgeteer.Blazor/Shared/ProcessingProgressDialog.razor create mode 100644 OpenBudgeteer.Core/Common/EventClasses/ProposeBucketChangedEventArgs.cs diff --git a/CHANGELOG.md b/CHANGELOG.md index 588e086..6af0b5e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,6 @@ ### 1.4 (20xx-xx-xx) +* [Add] Dialog for showing progress during Bucket proposal #21 * [Changed] Core and Blazor Frontend updated to .Net 5.0 * [Changed] File Preview on Import Page now read-only * [Changed] Misc small visual updates and fixes on Import Page diff --git a/OpenBudgeteer.Blazor/Pages/Transaction.razor b/OpenBudgeteer.Blazor/Pages/Transaction.razor index d1d54dd..197fe64 100644 --- a/OpenBudgeteer.Blazor/Pages/Transaction.razor +++ b/OpenBudgeteer.Blazor/Pages/Transaction.razor @@ -5,6 +5,7 @@ @using Microsoft.EntityFrameworkCore @using System.Drawing @using OpenBudgeteer.Core.Common +@using OpenBudgeteer.Core.Common.EventClasses @using OpenBudgeteer.Core.ViewModels.ItemViewModels @inject DbContextOptions DbContextOptions @inject YearMonthSelectorViewModel YearMonthDataContext @@ -21,7 +22,7 @@ { - + }
@@ -216,6 +217,10 @@ IsDialogVisible="@_isErrorModalDialogVisible" OnClickCallback="@(() => _isErrorModalDialogVisible = false)"/> + + @code { TransactionViewModel _dataContext; bool _newTransactionEnabled; @@ -227,6 +232,8 @@ bool _isErrorModalDialogVisible; string _errorModalDialogMessage; + ProcessingProgressDialog _proposeBucketsProcessingProgressDialog; + protected override async Task OnInitializedAsync() { _dataContext = new TransactionViewModel(DbContextOptions, YearMonthDataContext); @@ -254,8 +261,23 @@ void ProposeBuckets() { + _proposeBucketsProcessingProgressDialog.Message = "Propose Buckets for unassigned Transactions..."; + _proposeBucketsProcessingProgressDialog.MinValue = 0; + _proposeBucketsProcessingProgressDialog.MaxValue = _dataContext.ProposeBucketsCount; + _proposeBucketsProcessingProgressDialog.CurrentValue = _dataContext.ProposeBucketsProgress; + _proposeBucketsProcessingProgressDialog.CurrentPercentage = _dataContext.ProposeBucketsPercentage; + _proposeBucketsProcessingProgressDialog.Open(); + _dataContext.BucketProposalProgressChanged += OnBucketProposalProgressChanged; + _dataContext.ProposeBuckets(); if (_dataContext.Transactions.Any(i => i.InModification)) _massEditEnabled = true; + + _proposeBucketsProcessingProgressDialog.Close(); + } + + private void OnBucketProposalProgressChanged(object sender, ProposeBucketChangedEventArgs e) + { + _proposeBucketsProcessingProgressDialog.UpdateProgress(e.NewValue, e.NewProgress); } async void SaveAllTransaction() diff --git a/OpenBudgeteer.Blazor/Shared/ProcessingProgressDialog.razor b/OpenBudgeteer.Blazor/Shared/ProcessingProgressDialog.razor new file mode 100644 index 0000000..c88dbef --- /dev/null +++ b/OpenBudgeteer.Blazor/Shared/ProcessingProgressDialog.razor @@ -0,0 +1,52 @@ + + +@if (_showBackdrop) +{ + +} + +@code { + public string Message { get; set; } + public int MinValue { get; set; } + public int MaxValue { get; set; } + public int CurrentValue { get; set; } + public int CurrentPercentage { get; set; } + + string _modalDisplay = "none;"; + string _modalClass = ""; + bool _showBackdrop = false; + + public void Open() + { + _modalDisplay = "block;"; + _modalClass = "Show"; + _showBackdrop = true; + StateHasChanged(); + } + + public void Close() + { + _modalDisplay = "none"; + _modalClass = ""; + _showBackdrop = false; + StateHasChanged(); + } + + public void UpdateProgress(int newValue, int newProgress) + { + CurrentValue = newValue; + CurrentPercentage = newProgress; + StateHasChanged(); + } +} diff --git a/OpenBudgeteer.Blazor/appsettings.json b/OpenBudgeteer.Blazor/appsettings.json index 7e2a179..788081e 100644 --- a/OpenBudgeteer.Blazor/appsettings.json +++ b/OpenBudgeteer.Blazor/appsettings.json @@ -2,7 +2,7 @@ "Connection": { "Provider" : "mysql", "Database": "openbudgeteer-dev", - "Server": "192.168.178.93", + "Server": "host.docker.internal", "Port": "3306", "User": "openbudgeteer-dev", "Password": "openbudgeteer-dev" diff --git a/OpenBudgeteer.Core/Common/EventClasses/ProposeBucketChangedEventArgs.cs b/OpenBudgeteer.Core/Common/EventClasses/ProposeBucketChangedEventArgs.cs new file mode 100644 index 0000000..cfa4265 --- /dev/null +++ b/OpenBudgeteer.Core/Common/EventClasses/ProposeBucketChangedEventArgs.cs @@ -0,0 +1,20 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace OpenBudgeteer.Core.Common.EventClasses +{ + public class ProposeBucketChangedEventArgs : EventArgs + { + public int NewValue { get; private set; } + public int NewProgress { get; private set; } + + public ProposeBucketChangedEventArgs(int newValue, int newProgress) + { + NewValue = newValue; + NewProgress = newProgress; + } + } +} diff --git a/OpenBudgeteer.Core/ViewModels/TransactionViewModel.cs b/OpenBudgeteer.Core/ViewModels/TransactionViewModel.cs index 5166b93..a6c7013 100644 --- a/OpenBudgeteer.Core/ViewModels/TransactionViewModel.cs +++ b/OpenBudgeteer.Core/ViewModels/TransactionViewModel.cs @@ -28,6 +28,36 @@ public TransactionViewModelItem NewTransaction set => Set(ref _newTransaction, value); } + private int _proposeBucketsCount; + /// + /// Helper property for Progress Dialog during Bucket proposal process"/>/> + /// + public int ProposeBucketsCount + { + get => _proposeBucketsCount; + set => Set(ref _proposeBucketsCount, value); + } + + private int _proposeBucketsProgress; + /// + /// Helper property for Progress Dialog during Bucket proposal process"/>/> + /// + public int ProposeBucketsProgress + { + get => _proposeBucketsProgress; + set => Set(ref _proposeBucketsProgress, value); + } + + private int _proposeBucketsPercentage; + /// + /// Helper property for Progress Dialog during Bucket proposal process"/>/> + /// + public int ProposeBucketsPercentage + { + get => _proposeBucketsPercentage; + set => Set(ref _proposeBucketsPercentage, value); + } + private ObservableCollection _transactions; /// /// Collection of loaded Transactions @@ -38,6 +68,12 @@ public ObservableCollection Transactions set => Set(ref _transactions, value); } + /// + /// EventHandler which should be invoked during Bucket Proposal to track overall Progress + /// + public event EventHandler BucketProposalProgressChanged; + + private readonly DbContextOptions _dbOptions; private readonly YearMonthSelectorViewModel _yearMonthViewModel; @@ -266,14 +302,19 @@ public ViewModelOperationResult SaveAllTransaction() /// Sets all Transactions into Modification Mode in case they have a "No Selection" Bucket public void ProposeBuckets() { - foreach (var transaction in Transactions) + var unassignedTransactions = Transactions.Where(i => i.Buckets.First().SelectedBucket.BucketId == 0); + ProposeBucketsCount = unassignedTransactions.Count(); + ProposeBucketsProgress = 0; + + foreach (var transaction in unassignedTransactions) { - // Check on "No Selection" Bucket - if (transaction.Buckets.First().SelectedBucket.BucketId == 0) - { - transaction.StartModification(); - transaction.ProposeBucket(); - } + transaction.StartModification(); + transaction.ProposeBucket(); + ProposeBucketsProgress++; + ProposeBucketsPercentage = ProposeBucketsCount == 0 ? 0 : + Convert.ToInt32(Decimal.Divide(ProposeBucketsProgress, ProposeBucketsCount) * 100); + BucketProposalProgressChanged?.Invoke(this, + new ProposeBucketChangedEventArgs(ProposeBucketsProgress, ProposeBucketsPercentage)); } } } From 2b66393e2437e253a12c341cd31af20f0d0e6b25 Mon Sep 17 00:00:00 2001 From: Axelander Date: Mon, 30 Aug 2021 08:19:09 +0000 Subject: [PATCH 17/50] Update CHANGELOG.md --- CHANGELOG.md | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 6af0b5e..133c750 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,12 +5,12 @@ * [Changed] File Preview on Import Page now read-only * [Changed] Misc small visual updates and fixes on Import Page * [Changed] Consistent Chart Header styles on Report Page -* [Changed] Updated dependencies -* [Changed] Simplified dependency tree +* [Changed] Updated dependencies. Thanks @kekkon +* [Changed] Simplified dependency tree. Thanks @kekkon * [Fixed] Reworked UI update handling to fix issues on refreshing data #22 * [Fixed] Missing Target Account update for newly created or updated Import Profiles #23 -* [Fixed] `MonthOutputConverter.Convert` not using Culture -* [Fixed] `OpenBudgeteer.Core.Test.ViewModelTest.YearMonthSelectorViewModelTest.Constructor_CheckDefaults` test using thread culture +* [Fixed] `MonthOutputConverter.Convert` not using Culture. Thanks @kekkon +* [Fixed] `OpenBudgeteer.Core.Test.ViewModelTest.YearMonthSelectorViewModelTest.Constructor_CheckDefaults` test using thread culture. Thanks @kekkon ### 1.3 (2020-12-15) From 6a9739541f7f3f24cc7d13397448c7a6ea485fdd Mon Sep 17 00:00:00 2001 From: Axelander Date: Mon, 30 Aug 2021 09:19:52 +0000 Subject: [PATCH 18/50] Add Docker-Compose section in README.md --- README.md | 58 +++++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 58 insertions(+) diff --git a/README.md b/README.md index f32b693..da32304 100644 --- a/README.md +++ b/README.md @@ -56,6 +56,64 @@ docker run -d --name='openbudgeteer' \ 'axelander/openbudgeteer:pre-release' ``` +### Docker-Compose + +Below an example how to deploy OpenBudgeteer together with MySql Server and phpMyAdmin for administration. Please note that user and database `openbudgeteer` need to be availabe, otherwise the container `openbudgeteer` will not work. + +So what you can do this is running below Docker Compose, create user and database using phpMyAdmin and then restart either container `openbudgeteer` or the whole Docker Compose. + +``` +version: "3" + +networks: + app-global: + external: true + mysql-internal: + + +services: + openbudgeteer: + image: axelander/openbudgeteer + container_name: openbudgeteer + ports: + - 8081:80 + environment: + - Connection:Server=bucket-mysql + - Connection:Port=3306 + - Connection:Database=openbudgeteer + - Connection:User=openbudgeteer + - Connection:Password=openbudgeteer + depends_on: + - bucket-mysql + networks: + - app-global + - mysql-internal + + bucket-mysql: + image: mysql + container_name: bucket-mysql + environment: + MYSQL_ROOT_PASSWORD: myRootPassword + volumes: + - data:/var/lib/mysql + networks: + - mysql-internal + + phpmyadmin: + image: phpmyadmin/phpmyadmin + container_name: bucket-phpmyadmin + links: + - bucket-mysql:db + ports: + - 8080:80 + networks: + - app-global + - mysql-internal + +volumes: + data: +``` + ## How to use ### Create Bank Account From 8d1a7d2a2d8538f76fe92e1e814d2b62c377de31 Mon Sep 17 00:00:00 2001 From: Axelander Date: Mon, 30 Aug 2021 09:22:44 +0000 Subject: [PATCH 19/50] Add syntax highlight to code sections in README.md --- README.md | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/README.md b/README.md index da32304..050878d 100644 --- a/README.md +++ b/README.md @@ -17,7 +17,7 @@ You can use the pre-built Docker Image from [Docker Hub](https://hub.docker.com/ | Connection:User | Database user | MyOpenBudgeteerUser | | Connection:Password | Database password | MyOpenBudgeteerPassword | -``` +```bash docker run -d --name='openbudgeteer' \ -e 'Connection:Provider'='mysql' \ -e 'Connection:Server'='192.168.178.100' \ @@ -31,7 +31,7 @@ docker run -d --name='openbudgeteer' \ Alternatively you can use a local `Sqlite` database using the below settings: -``` +```bash docker run -d --name='openbudgeteer' \ -e 'Connection:Provider'='sqlite' \ -v '/my/local/path:/app/database' \ @@ -44,7 +44,7 @@ If you don't change the Port Mapping you can access the App with Port `80`. Othe A Pre-Release version can be used with the Tag `pre-release` -``` +```bash docker run -d --name='openbudgeteer' \ -e 'Connection:Provider'='mysql' \ -e 'Connection:Server'='192.168.178.100' \ @@ -62,7 +62,7 @@ Below an example how to deploy OpenBudgeteer together with MySql Server and phpM So what you can do this is running below Docker Compose, create user and database using phpMyAdmin and then restart either container `openbudgeteer` or the whole Docker Compose. -``` +```yml version: "3" networks: From d4bb288baecaceb2a642fff2fb91dee1316eb572 Mon Sep 17 00:00:00 2001 From: Axelander Date: Mon, 30 Aug 2021 09:28:48 +0000 Subject: [PATCH 20/50] Update Docker-Compose section in README.md --- README.md | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/README.md b/README.md index 050878d..3f7fce3 100644 --- a/README.md +++ b/README.md @@ -78,20 +78,20 @@ services: ports: - 8081:80 environment: - - Connection:Server=bucket-mysql + - Connection:Server=openbudgeteer-mysql - Connection:Port=3306 - Connection:Database=openbudgeteer - Connection:User=openbudgeteer - Connection:Password=openbudgeteer depends_on: - - bucket-mysql + - mysql networks: - app-global - mysql-internal - bucket-mysql: + mysql: image: mysql - container_name: bucket-mysql + container_name: openbudgeteer-mysql environment: MYSQL_ROOT_PASSWORD: myRootPassword volumes: @@ -101,9 +101,9 @@ services: phpmyadmin: image: phpmyadmin/phpmyadmin - container_name: bucket-phpmyadmin + container_name: openbudgeteer-phpmyadmin links: - - bucket-mysql:db + - mysql:db ports: - 8080:80 networks: From cb41841423226ce03ef6bd9567e21c8ddc00c713 Mon Sep 17 00:00:00 2001 From: Axelander Date: Wed, 22 Sep 2021 11:43:22 +0200 Subject: [PATCH 21/50] Added Validation checks before saving Bucket data #29 --- .../ItemViewModels/BucketViewModelItem.cs | 37 +++++++++++++++++-- 1 file changed, 34 insertions(+), 3 deletions(-) diff --git a/OpenBudgeteer.Core/ViewModels/ItemViewModels/BucketViewModelItem.cs b/OpenBudgeteer.Core/ViewModels/ItemViewModels/BucketViewModelItem.cs index 84f2137..764f263 100644 --- a/OpenBudgeteer.Core/ViewModels/ItemViewModels/BucketViewModelItem.cs +++ b/OpenBudgeteer.Core/ViewModels/ItemViewModels/BucketViewModelItem.cs @@ -9,6 +9,7 @@ using System.Text; using Microsoft.EntityFrameworkCore; using System.Threading.Tasks; +using Microsoft.EntityFrameworkCore.Design; using OpenBudgeteer.Core.Common; using OpenBudgeteer.Core.Common.EventClasses; @@ -403,7 +404,7 @@ decimal CalculateWant(DateTime targetDate) #region Details - if (BucketVersion.BucketType == 3 || BucketVersion.BucketType == 4) + if (BucketVersion.BucketType is 3 or 4) { var targetDate = BucketVersion.BucketTypeZParam; // Calculate new target date for BucketType 3 (Expense every X Months) @@ -511,13 +512,43 @@ public ViewModelOperationResult CloseBucket() /// Object which contains information and results of this method public ViewModelOperationResult CreateOrUpdateBucket() { - var result = _isNewlyCreatedBucket ? CreateBucket() : UpdateBucket(); - if (!result.IsSuccessful || result.ViewModelReloadRequired) return result; + var validationResult = ValidateData(); + if (!validationResult.IsSuccessful) return validationResult; + var writeDataResult = _isNewlyCreatedBucket ? CreateBucket() : UpdateBucket(); + if (!writeDataResult.IsSuccessful || writeDataResult.ViewModelReloadRequired) return writeDataResult; InModification = false; CalculateValues(); return new ViewModelOperationResult(true); } + /// + /// Runs several validation rules to prevent unintended behavior + /// + /// Object which contains information and results of this method + private ViewModelOperationResult ValidateData() + { + try + { + // Check if target amount is positive + if (BucketVersion.BucketTypeYParam < 0) + { + throw new Exception("Target amount must be positive"); + } + + // Check if target amount is 0 to prevent DivideByZeroException + if ((BucketVersion.BucketType is 3 or 4) && BucketVersion.BucketTypeYParam == 0) + { + throw new Exception("Target amount must not be 0 for this Bucket Type."); + } + } + catch (Exception e) + { + return new ViewModelOperationResult(false, e.Message); + } + + return new ViewModelOperationResult(true); + } + /// /// Creates a new record in the database based on object /// From 4ed7a5f01854b5bd7a67e13b06b2f7f0dd2b3852 Mon Sep 17 00:00:00 2001 From: Axelander Date: Wed, 22 Sep 2021 14:30:21 +0200 Subject: [PATCH 22/50] Added special Progress calculation for Buckets with Save X Until Y where target amount has been reached and there is an expense in target month --- .../ViewModelTest/BucketViewModelTest.cs | 42 ++++++++++++++++--- .../ItemViewModels/BucketViewModelItem.cs | 19 ++++++++- 2 files changed, 55 insertions(+), 6 deletions(-) diff --git a/OpenBudgeteer.Core.Test/ViewModelTest/BucketViewModelTest.cs b/OpenBudgeteer.Core.Test/ViewModelTest/BucketViewModelTest.cs index de7ce3c..0545c07 100644 --- a/OpenBudgeteer.Core.Test/ViewModelTest/BucketViewModelTest.cs +++ b/OpenBudgeteer.Core.Test/ViewModelTest/BucketViewModelTest.cs @@ -658,7 +658,7 @@ public static IEnumerable TestData_CheckWantAndDetailCalculation_Expen }; } } - + public static IEnumerable TestData_CheckWantAndDetailCalculation_SaveXUntilY { get @@ -760,11 +760,43 @@ public static IEnumerable TestData_CheckWantAndDetailCalculation_SaveX }, new object[] { - new Bucket { Name = "30 until 2009-12, target reached, with expense in target month", ValidFrom = new DateTime(2009,7,1) }, - new BucketVersion { Version = 1, BucketType = 4, BucketTypeYParam = 30, BucketTypeZParam = new DateTime(2009,12,1) }, + new Bucket { Name = "30 until 2010-01, target reached, with expense in target month", ValidFrom = new DateTime(2009,7,1) }, + new BucketVersion { Version = 1, BucketType = 4, BucketTypeYParam = 30, BucketTypeZParam = new DateTime(2010,1,1) }, + new List + { + new BankTransaction { TransactionDate = new DateTime(2010,1,5), Amount = -30 } + }, + new List + { + new BucketMovement { MovementDate = new DateTime(2009,7,1), Amount = 10 }, + new BucketMovement { MovementDate = new DateTime(2009,9,1), Amount = 10 }, + new BucketMovement { MovementDate = new DateTime(2009,10,1), Amount = 10 } + }, + 0, 0, -30, 0, "30 until 2010-01", 100 + }, + new object[] + { + new Bucket { Name = "30 until 2010-01, target reached, with lower expense in target month", ValidFrom = new DateTime(2009,7,1) }, + new BucketVersion { Version = 1, BucketType = 4, BucketTypeYParam = 30, BucketTypeZParam = new DateTime(2010,1,1) }, + new List + { + new BankTransaction { TransactionDate = new DateTime(2010,1,5), Amount = -20 } + }, + new List + { + new BucketMovement { MovementDate = new DateTime(2009,7,1), Amount = 10 }, + new BucketMovement { MovementDate = new DateTime(2009,9,1), Amount = 10 }, + new BucketMovement { MovementDate = new DateTime(2009,10,1), Amount = 10 } + }, + 0, 0, -20, 10, "30 until 2010-01", 100 + }, + new object[] + { + new Bucket { Name = "30 until 2010-01, target reached, with higher expense in target month", ValidFrom = new DateTime(2009,7,1) }, + new BucketVersion { Version = 1, BucketType = 4, BucketTypeYParam = 30, BucketTypeZParam = new DateTime(2010,1,1) }, new List { - new BankTransaction { TransactionDate = new DateTime(2009,12,1), Amount = -30 } + new BankTransaction { TransactionDate = new DateTime(2010,1,5), Amount = -40 } }, new List { @@ -772,7 +804,7 @@ public static IEnumerable TestData_CheckWantAndDetailCalculation_SaveX new BucketMovement { MovementDate = new DateTime(2009,9,1), Amount = 10 }, new BucketMovement { MovementDate = new DateTime(2009,10,1), Amount = 10 } }, - 0, 0, 0, 0, "30 until 2009-12", 100 + 10, 0, -40, -10, "30 until 2010-01", 75 } }; } diff --git a/OpenBudgeteer.Core/ViewModels/ItemViewModels/BucketViewModelItem.cs b/OpenBudgeteer.Core/ViewModels/ItemViewModels/BucketViewModelItem.cs index 764f263..e108b43 100644 --- a/OpenBudgeteer.Core/ViewModels/ItemViewModels/BucketViewModelItem.cs +++ b/OpenBudgeteer.Core/ViewModels/ItemViewModels/BucketViewModelItem.cs @@ -417,7 +417,24 @@ decimal CalculateWant(DateTime targetDate) } while (targetDate < _currentYearMonth); } - Progress = Convert.ToInt32((Balance / BucketVersion.BucketTypeYParam) * 100); + // Special Progress handling in target month with available activity, otherwise usual calculation + if (_currentYearMonth.Month == targetDate.Month && + _currentYearMonth.Year == targetDate.Year && + Activity < 0) + { + Progress = Balance >= 0 ? + // Expense as expected or lower, hence target reached and Progress 100 + 100 : + // Expense in target month was higher than expected, hence negative Balance. + // Progress based on Want and Activity + Convert.ToInt32(100 - (Want / Activity * -1) * 100); + + } + else + { + Progress = Convert.ToInt32((Balance / BucketVersion.BucketTypeYParam) * 100); + } + Details = $"{BucketVersion.BucketTypeYParam} until {targetDate:yyyy-MM}"; IsProgressbarVisible = true; } From a586e90ef5aa93e2fe89b98451dee5c7cc1f0fde Mon Sep 17 00:00:00 2001 From: Axelander Date: Wed, 22 Sep 2021 14:33:49 +0200 Subject: [PATCH 23/50] For Buckets with over-fulfilled target Progress will be set to 100 #26 --- .../ViewModels/ItemViewModels/BucketViewModelItem.cs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/OpenBudgeteer.Core/ViewModels/ItemViewModels/BucketViewModelItem.cs b/OpenBudgeteer.Core/ViewModels/ItemViewModels/BucketViewModelItem.cs index e108b43..c22ac68 100644 --- a/OpenBudgeteer.Core/ViewModels/ItemViewModels/BucketViewModelItem.cs +++ b/OpenBudgeteer.Core/ViewModels/ItemViewModels/BucketViewModelItem.cs @@ -432,7 +432,8 @@ decimal CalculateWant(DateTime targetDate) } else { - Progress = Convert.ToInt32((Balance / BucketVersion.BucketTypeYParam) * 100); + Progress = Convert.ToInt32((Balance / BucketVersion.BucketTypeYParam) * 100); + if (Progress > 100) Progress = 100; } Details = $"{BucketVersion.BucketTypeYParam} until {targetDate:yyyy-MM}"; From 69d90bc8fd90754f601ae59fad097ef2ff5852c1 Mon Sep 17 00:00:00 2001 From: Axelander Date: Wed, 22 Sep 2021 14:42:19 +0200 Subject: [PATCH 24/50] Fix Test Case #27 --- OpenBudgeteer.Core.Test/ViewModelTest/BucketViewModelTest.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/OpenBudgeteer.Core.Test/ViewModelTest/BucketViewModelTest.cs b/OpenBudgeteer.Core.Test/ViewModelTest/BucketViewModelTest.cs index 0545c07..b2715ce 100644 --- a/OpenBudgeteer.Core.Test/ViewModelTest/BucketViewModelTest.cs +++ b/OpenBudgeteer.Core.Test/ViewModelTest/BucketViewModelTest.cs @@ -756,7 +756,7 @@ public static IEnumerable TestData_CheckWantAndDetailCalculation_SaveX new BucketMovement { MovementDate = new DateTime(2009,9,1), Amount = 10 }, new BucketMovement { MovementDate = new DateTime(2009,10,1), Amount = 10 } }, - 90, 0, 0, 30, "120 until 2009-12", 25 + 0, 0, 0, 30, "120 until 2009-12", 25 }, new object[] { From 7187e1584903ba87cff0a7e8555d913f44e2c2ad Mon Sep 17 00:00:00 2001 From: Axelander Date: Wed, 22 Sep 2021 12:54:52 +0000 Subject: [PATCH 25/50] Update CHANGELOG.md --- CHANGELOG.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 133c750..d231978 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,10 +7,12 @@ * [Changed] Consistent Chart Header styles on Report Page * [Changed] Updated dependencies. Thanks @kekkon * [Changed] Simplified dependency tree. Thanks @kekkon +* [Changed] Progress calculation for several scenarios #26 #28 * [Fixed] Reworked UI update handling to fix issues on refreshing data #22 * [Fixed] Missing Target Account update for newly created or updated Import Profiles #23 * [Fixed] `MonthOutputConverter.Convert` not using Culture. Thanks @kekkon * [Fixed] `OpenBudgeteer.Core.Test.ViewModelTest.YearMonthSelectorViewModelTest.Constructor_CheckDefaults` test using thread culture. Thanks @kekkon +* [Fixed] Added Validation checks before saving Bucket data to fix DivideByZeroException #29 ### 1.3 (2020-12-15) From 3bc211920915eb3b0354fb035542eb8a61dfebb2 Mon Sep 17 00:00:00 2001 From: Axelander Date: Wed, 22 Sep 2021 15:00:01 +0200 Subject: [PATCH 26/50] Cleanup using --- .../ViewModels/ItemViewModels/BucketViewModelItem.cs | 4 ---- 1 file changed, 4 deletions(-) diff --git a/OpenBudgeteer.Core/ViewModels/ItemViewModels/BucketViewModelItem.cs b/OpenBudgeteer.Core/ViewModels/ItemViewModels/BucketViewModelItem.cs index c22ac68..d07f53f 100644 --- a/OpenBudgeteer.Core/ViewModels/ItemViewModels/BucketViewModelItem.cs +++ b/OpenBudgeteer.Core/ViewModels/ItemViewModels/BucketViewModelItem.cs @@ -1,17 +1,13 @@ using OpenBudgeteer.Core.Common.Database; using OpenBudgeteer.Core.Models; using System; -using System.Collections.Generic; using System.Collections.ObjectModel; using System.Drawing; using System.Linq; using System.Reflection; -using System.Text; using Microsoft.EntityFrameworkCore; using System.Threading.Tasks; -using Microsoft.EntityFrameworkCore.Design; using OpenBudgeteer.Core.Common; -using OpenBudgeteer.Core.Common.EventClasses; namespace OpenBudgeteer.Core.ViewModels.ItemViewModels { From 3246450f2901c633894059cc7749467f4218bd53 Mon Sep 17 00:00:00 2001 From: Alexander Preibisch Date: Sat, 6 Nov 2021 15:03:32 +0100 Subject: [PATCH 27/50] Add Filter on Transaction View #25 --- CHANGELOG.md | 1 + OpenBudgeteer.Blazor/Pages/Transaction.razor | 18 ++++- .../Common/Extensions/EnumExtensions.cs | 29 ++++++++ .../Common/Extensions/StringValueAttribute.cs | 24 +++++++ .../ViewModels/TransactionViewModel.cs | 71 +++++++++++++++---- 5 files changed, 127 insertions(+), 16 deletions(-) create mode 100644 OpenBudgeteer.Core/Common/Extensions/EnumExtensions.cs create mode 100644 OpenBudgeteer.Core/Common/Extensions/StringValueAttribute.cs diff --git a/CHANGELOG.md b/CHANGELOG.md index d231978..6b006ad 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,6 +1,7 @@ ### 1.4 (20xx-xx-xx) * [Add] Dialog for showing progress during Bucket proposal #21 +* [Add] Filter on Transaction Page #25 * [Changed] Core and Blazor Frontend updated to .Net 5.0 * [Changed] File Preview on Import Page now read-only * [Changed] Misc small visual updates and fixes on Import Page diff --git a/OpenBudgeteer.Blazor/Pages/Transaction.razor b/OpenBudgeteer.Blazor/Pages/Transaction.razor index 197fe64..9db3818 100644 --- a/OpenBudgeteer.Blazor/Pages/Transaction.razor +++ b/OpenBudgeteer.Blazor/Pages/Transaction.razor @@ -6,12 +6,13 @@ @using System.Drawing @using OpenBudgeteer.Core.Common @using OpenBudgeteer.Core.Common.EventClasses +@using OpenBudgeteer.Core.Common.Extensions @using OpenBudgeteer.Core.ViewModels.ItemViewModels @inject DbContextOptions DbContextOptions @inject YearMonthSelectorViewModel YearMonthDataContext -
+
@if (_massEditEnabled) { @@ -25,6 +26,14 @@ }
+
+ +
@@ -290,12 +299,19 @@ { _massEditEnabled = false; await HandleResult(await _dataContext.LoadDataAsync()); + base.StateHasChanged(); } async void SaveTransaction(TransactionViewModelItem transaction) { await HandleResult(transaction.UpdateItem()); } + + void Filter_SelectionChanged(ChangeEventArgs e) + { + _dataContext.CurrentFilter = Enum.Parse( + e.Value as string ?? TransactionViewModelFilter.NoFilter.ToString()); + } void HandleTransactionDeletionRequest(TransactionViewModelItem transaction) { diff --git a/OpenBudgeteer.Core/Common/Extensions/EnumExtensions.cs b/OpenBudgeteer.Core/Common/Extensions/EnumExtensions.cs new file mode 100644 index 0000000..a161765 --- /dev/null +++ b/OpenBudgeteer.Core/Common/Extensions/EnumExtensions.cs @@ -0,0 +1,29 @@ +using System; + +namespace OpenBudgeteer.Core.Common.Extensions +{ + public static class EnumExtensions + { + /// + /// Will get the string value for a given enums value, this will + /// only work if you assign the StringValue attribute to + /// the items in your enum. + /// + /// + /// + public static string GetStringValue(this Enum value) { + // Get the type + var type = value.GetType(); + + // Get fieldinfo for this type + var fieldInfo = type.GetField(value.ToString()); + + // Get the stringvalue attributes + var attribs = fieldInfo?.GetCustomAttributes( + typeof(StringValueAttribute), false) as StringValueAttribute[]; + + // Return the first if there was a match. + return attribs?.Length > 0 ? attribs[0].StringValue : null; + } + } +} \ No newline at end of file diff --git a/OpenBudgeteer.Core/Common/Extensions/StringValueAttribute.cs b/OpenBudgeteer.Core/Common/Extensions/StringValueAttribute.cs new file mode 100644 index 0000000..1d51b24 --- /dev/null +++ b/OpenBudgeteer.Core/Common/Extensions/StringValueAttribute.cs @@ -0,0 +1,24 @@ +using System; + +namespace OpenBudgeteer.Core.Common.Extensions +{ + /// + /// This attribute is used to represent a string value for a value in an enum. + /// + public class StringValueAttribute : Attribute { + + /// + /// Holds the stringvalue for a value in an enum. + /// + public string StringValue { get; protected set; } + + /// + /// Constructor used to init a StringValue Attribute + /// + /// + public StringValueAttribute(string value) + { + this.StringValue = value; + } + } +} \ No newline at end of file diff --git a/OpenBudgeteer.Core/ViewModels/TransactionViewModel.cs b/OpenBudgeteer.Core/ViewModels/TransactionViewModel.cs index a6c7013..35d1fe4 100644 --- a/OpenBudgeteer.Core/ViewModels/TransactionViewModel.cs +++ b/OpenBudgeteer.Core/ViewModels/TransactionViewModel.cs @@ -13,9 +13,23 @@ using Microsoft.EntityFrameworkCore; using OpenBudgeteer.Core.Common; using OpenBudgeteer.Core.Common.EventClasses; +using OpenBudgeteer.Core.Common.Extensions; namespace OpenBudgeteer.Core.ViewModels { + /// + /// Identifier which kind of filter can be applied on the + /// + public enum TransactionViewModelFilter: int + { + [StringValue("No Filter")] + NoFilter = 0, + [StringValue("Hide mapped")] + HideMapped = 1, + [StringValue("Only mapped")] + OnlyMapped = 2 + } + public class TransactionViewModel : ViewModelBase { private TransactionViewModelItem _newTransaction; @@ -27,10 +41,10 @@ public TransactionViewModelItem NewTransaction get => _newTransaction; set => Set(ref _newTransaction, value); } - + private int _proposeBucketsCount; /// - /// Helper property for Progress Dialog during Bucket proposal process"/>/> + /// Helper property for Progress Dialog during Bucket proposal process /// public int ProposeBucketsCount { @@ -40,7 +54,7 @@ public int ProposeBucketsCount private int _proposeBucketsProgress; /// - /// Helper property for Progress Dialog during Bucket proposal process"/>/> + /// Helper property for Progress Dialog during Bucket proposal process /// public int ProposeBucketsProgress { @@ -50,13 +64,27 @@ public int ProposeBucketsProgress private int _proposeBucketsPercentage; /// - /// Helper property for Progress Dialog during Bucket proposal process"/>/> + /// Helper property for Progress Dialog during Bucket proposal process /// public int ProposeBucketsPercentage { get => _proposeBucketsPercentage; set => Set(ref _proposeBucketsPercentage, value); } + + private TransactionViewModelFilter _currentFilter; + /// + /// Sets the current filter for the ViewModel + /// + public TransactionViewModelFilter CurrentFilter + { + get => _currentFilter; + set + { + if (Set(ref _currentFilter, value)) + NotifyPropertyChanged(nameof(Transactions)); + } + } private ObservableCollection _transactions; /// @@ -64,16 +92,29 @@ public int ProposeBucketsPercentage /// public ObservableCollection Transactions { - get => _transactions; - set => Set(ref _transactions, value); + get + { + switch (CurrentFilter) + { + case TransactionViewModelFilter.HideMapped: + return new ObservableCollection( + _transactions.Where(i => i.Buckets.First().SelectedBucket.BucketId == 0)); + case TransactionViewModelFilter.OnlyMapped: + return new ObservableCollection( + _transactions.Where(i => i.Buckets.First().SelectedBucket.BucketId > 0)); + case TransactionViewModelFilter.NoFilter: + default: + return _transactions; + } + } + protected set => Set(ref _transactions, value); } - + /// /// EventHandler which should be invoked during Bucket Proposal to track overall Progress /// public event EventHandler BucketProposalProgressChanged; - private readonly DbContextOptions _dbOptions; private readonly YearMonthSelectorViewModel _yearMonthViewModel; @@ -87,7 +128,7 @@ public TransactionViewModel(DbContextOptions dbOptions, YearMon _dbOptions = dbOptions; _yearMonthViewModel = yearMonthViewModel; ResetNewTransaction(); - Transactions = new ObservableCollection(); + _transactions = new ObservableCollection(); //_yearMonthViewModel.SelectedYearMonthChanged += (sender) => { LoadData(); }; } @@ -101,7 +142,7 @@ public async Task LoadDataAsync() { // Get all available transactions. The TransactionViewModelItem takes care to find all assigned buckets for // each passed transaction. It creates also the respective ViewModelObjects - Transactions.Clear(); + _transactions.Clear(); using (var dbContext = new DatabaseContext(_dbOptions)) { @@ -119,7 +160,7 @@ public async Task LoadDataAsync() foreach (var transaction in await Task.WhenAll(transactionTasks)) { - Transactions.Add(transaction); + _transactions.Add(transaction); } return new ViewModelOperationResult(true); @@ -142,7 +183,7 @@ public async Task LoadDataAsync(Bucket bucket, bool wi { try { - Transactions.Clear(); + _transactions.Clear(); using (var dbContext = new DatabaseContext(_dbOptions)) { @@ -183,7 +224,7 @@ public async Task LoadDataAsync(Bucket bucket, bool wi foreach (var transaction in (await Task.WhenAll(transactionTasks)) .OrderByDescending(i => i.Transaction.TransactionDate)) { - Transactions.Add(transaction); + _transactions.Add(transaction); } return new ViewModelOperationResult(true); @@ -205,7 +246,7 @@ public async Task LoadDataAsync(Account account) { try { - Transactions.Clear(); + _transactions.Clear(); using (var dbContext = new DatabaseContext(_dbOptions)) { var results = @@ -223,7 +264,7 @@ public async Task LoadDataAsync(Account account) foreach (var transaction in await Task.WhenAll(transactionTasks)) { - Transactions.Add(transaction); + _transactions.Add(transaction); } return new ViewModelOperationResult(true); From 91ac8d853609528d9dd612002e24fa18a3a71bd4 Mon Sep 17 00:00:00 2001 From: Alexander Preibisch Date: Sat, 6 Nov 2021 16:51:16 +0100 Subject: [PATCH 28/50] Optimized Mass Edit handling with new Filters --- OpenBudgeteer.Blazor/Pages/Transaction.razor | 12 +++++++-- .../TransactionViewModelItem.cs | 2 +- .../ViewModels/TransactionViewModel.cs | 27 ++++++++++++++++--- 3 files changed, 34 insertions(+), 7 deletions(-) diff --git a/OpenBudgeteer.Blazor/Pages/Transaction.razor b/OpenBudgeteer.Blazor/Pages/Transaction.razor index 9db3818..e3fd825 100644 --- a/OpenBudgeteer.Blazor/Pages/Transaction.razor +++ b/OpenBudgeteer.Blazor/Pages/Transaction.razor @@ -30,7 +30,15 @@
@@ -298,7 +306,7 @@ async void CancelAllTransaction() { _massEditEnabled = false; - await HandleResult(await _dataContext.LoadDataAsync()); + await HandleResult(await _dataContext.CancelAllTransactionAsync()); base.StateHasChanged(); } diff --git a/OpenBudgeteer.Core/ViewModels/ItemViewModels/TransactionViewModelItem.cs b/OpenBudgeteer.Core/ViewModels/ItemViewModels/TransactionViewModelItem.cs index b8d705a..89f0784 100644 --- a/OpenBudgeteer.Core/ViewModels/ItemViewModels/TransactionViewModelItem.cs +++ b/OpenBudgeteer.Core/ViewModels/ItemViewModels/TransactionViewModelItem.cs @@ -516,7 +516,7 @@ public ViewModelOperationResult UpdateItem() _oldTransactionViewModelItem = null; InModification = false; - return new ViewModelOperationResult(true, true); + return new ViewModelOperationResult(true, false); } public ViewModelOperationResult DeleteItem() diff --git a/OpenBudgeteer.Core/ViewModels/TransactionViewModel.cs b/OpenBudgeteer.Core/ViewModels/TransactionViewModel.cs index 35d1fe4..eaf78e5 100644 --- a/OpenBudgeteer.Core/ViewModels/TransactionViewModel.cs +++ b/OpenBudgeteer.Core/ViewModels/TransactionViewModel.cs @@ -27,7 +27,9 @@ public enum TransactionViewModelFilter: int [StringValue("Hide mapped")] HideMapped = 1, [StringValue("Only mapped")] - OnlyMapped = 2 + OnlyMapped = 2, + [StringValue("In Modification")] + InModification = 3, } public class TransactionViewModel : ViewModelBase @@ -102,6 +104,9 @@ public ObservableCollection Transactions case TransactionViewModelFilter.OnlyMapped: return new ObservableCollection( _transactions.Where(i => i.Buckets.First().SelectedBucket.BucketId > 0)); + case TransactionViewModelFilter.InModification: + return new ObservableCollection( + _transactions.Where(i => i.InModification)); case TransactionViewModelFilter.NoFilter: default: return _transactions; @@ -301,7 +306,7 @@ public void ResetNewTransaction() } /// - /// Helper method to start modification process for all Transactions + /// Helper method to start modification process for all Transactions based on current Filter /// public void EditAllTransaction() { @@ -309,6 +314,8 @@ public void EditAllTransaction() { transaction.StartModification(); } + + CurrentFilter = TransactionViewModelFilter.InModification; } /// @@ -321,12 +328,13 @@ public ViewModelOperationResult SaveAllTransaction() { try { - foreach (var transaction in Transactions) + foreach (var transaction in _transactions.Where(i => i.InModification)) { var result = transaction.UpdateItem(); if (!result.IsSuccessful) throw new Exception(result.Message); } dbTransaction.Commit(); + CurrentFilter = TransactionViewModelFilter.NoFilter; return new ViewModelOperationResult(true); } catch (Exception e) @@ -337,13 +345,24 @@ public ViewModelOperationResult SaveAllTransaction() } } + /// + /// Cancels update process for all Transactions. Reloads ViewModel to restore data. + /// + /// Object which contains information and results of this method + public async Task CancelAllTransactionAsync() + { + CurrentFilter = TransactionViewModelFilter.NoFilter; + return await LoadDataAsync(); + } + /// /// Starts process to propose the right for all Transactions /// /// Sets all Transactions into Modification Mode in case they have a "No Selection" Bucket public void ProposeBuckets() { - var unassignedTransactions = Transactions.Where(i => i.Buckets.First().SelectedBucket.BucketId == 0); + CurrentFilter = TransactionViewModelFilter.InModification; + var unassignedTransactions = _transactions.Where(i => i.Buckets.First().SelectedBucket.BucketId == 0); ProposeBucketsCount = unassignedTransactions.Count(); ProposeBucketsProgress = 0; From e9d65f75cbcc6e8f1617bbe420dbf5fba4fb6f3d Mon Sep 17 00:00:00 2001 From: Alexander Preibisch Date: Sat, 6 Nov 2021 16:59:39 +0100 Subject: [PATCH 29/50] Fixed Trigger of SelectedYearMonthChanged --- CHANGELOG.md | 1 + .../ViewModels/YearMonthSelectorViewModel.cs | 8 ++++---- 2 files changed, 5 insertions(+), 4 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 6b006ad..248b37d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -14,6 +14,7 @@ * [Fixed] `MonthOutputConverter.Convert` not using Culture. Thanks @kekkon * [Fixed] `OpenBudgeteer.Core.Test.ViewModelTest.YearMonthSelectorViewModelTest.Constructor_CheckDefaults` test using thread culture. Thanks @kekkon * [Fixed] Added Validation checks before saving Bucket data to fix DivideByZeroException #29 +* [Fixed] Trigger of `SelectedYearMonthChanged` passing `OpenBudgeteer.Core.Test.ViewModelTest.SelectedYearMonthChanged_CheckEventHasBeenInvoked` Test ### 1.3 (2020-12-15) diff --git a/OpenBudgeteer.Core/ViewModels/YearMonthSelectorViewModel.cs b/OpenBudgeteer.Core/ViewModels/YearMonthSelectorViewModel.cs index 89d3736..d804f52 100644 --- a/OpenBudgeteer.Core/ViewModels/YearMonthSelectorViewModel.cs +++ b/OpenBudgeteer.Core/ViewModels/YearMonthSelectorViewModel.cs @@ -18,8 +18,8 @@ public int SelectedMonth get => _selectedMonth; set { - Set(ref _selectedMonth, value); - if (!_yearMontIsChanging) SelectedYearMonthChanged?.Invoke(this, new ViewModelReloadEventArgs(this)); + var valueChanged = Set(ref _selectedMonth, value); + if (!_yearMontIsChanging && valueChanged) SelectedYearMonthChanged?.Invoke(this, new ViewModelReloadEventArgs(this)); } } @@ -33,8 +33,8 @@ public int SelectedYear get => _selectedYear; set { - Set(ref _selectedYear, value); - if (!_yearMontIsChanging) SelectedYearMonthChanged?.Invoke(this, new ViewModelReloadEventArgs(this)); + var valueChanged = Set(ref _selectedYear, value); + if (!_yearMontIsChanging && valueChanged) SelectedYearMonthChanged?.Invoke(this, new ViewModelReloadEventArgs(this)); } } From f2e3e8fa536e77d3c2c09bd7d0e4530473a225f7 Mon Sep 17 00:00:00 2001 From: Alexander Preibisch Date: Fri, 12 Nov 2021 16:38:10 +0100 Subject: [PATCH 30/50] Update to .Net 6 --- OpenBudgeteer.Blazor/Dockerfile | 4 +- .../OpenBudgeteer.Blazor.csproj | 2 +- OpenBudgeteer.Blazor/Program.cs | 33 +- OpenBudgeteer.Blazor/Startup.cs | 157 +- .../ViewModels/BlazorReportViewModel.cs | 511 +++--- OpenBudgeteer.Blazor/appsettings.json | 2 +- OpenBudgeteer.Core.Test/DbConnector.cs | 69 +- .../OpenBudgeteer.Core.Test.csproj | 4 +- .../AccountViewModelIsolatedTest.cs | 59 +- .../ViewModelTest/AccountViewModelTest.cs | 98 +- .../BucketViewModelIsolatedTest.cs | 225 ++- .../ViewModelTest/BucketViewModelTest.cs | 1533 ++++++++--------- .../YearMonthSelectorViewModelTest.cs | 209 ++- .../Common/Database/DatabaseContext.cs | 508 +++--- .../Common/Database/MySqlDatabaseContext.cs | 25 +- .../Database/MySqlDatabaseContextFactory.cs | 80 +- .../Common/Database/SqliteDatabaseContext.cs | 17 +- .../Database/SqliteDatabaseContextFactory.cs | 37 +- .../Common/EventClasses/AmountChangedArgs.cs | 30 +- .../DeleteAssignmentRequestArgs.cs | 26 +- .../ProposeBucketChangedEventArgs.cs | 23 +- .../EventClasses/ViewModelReloadEventArgs.cs | 17 +- .../Common/MonthOutputConverter.cs | 39 +- .../Common/ViewModelOperationResult.cs | 37 +- OpenBudgeteer.Core/Models/Account.cs | 41 +- OpenBudgeteer.Core/Models/BankTransaction.cs | 84 +- OpenBudgeteer.Core/Models/BaseObject.cs | 33 +- OpenBudgeteer.Core/Models/Bucket.cs | 99 +- OpenBudgeteer.Core/Models/BucketGroup.cs | 47 +- OpenBudgeteer.Core/Models/BucketMovement.cs | 72 +- OpenBudgeteer.Core/Models/BucketRuleSet.cs | 62 +- OpenBudgeteer.Core/Models/BucketVersion.cs | 224 ++- .../Models/BudgetedTransaction.cs | 62 +- OpenBudgeteer.Core/Models/ImportProfile.cs | 151 +- OpenBudgeteer.Core/Models/MappingRule.cs | 184 +- OpenBudgeteer.Core/OpenBudgeteer.Core.csproj | 2 +- .../ViewModels/AccountViewModel.cs | 142 +- .../ViewModels/BucketViewModel.cs | 643 ++++--- .../ViewModels/ImportDataViewModel.cs | 830 +++++---- .../ItemViewModels/AccountViewModelItem.cs | 171 +- .../BucketGroupViewModelItem.cs | 330 ++-- .../ItemViewModels/BucketViewModelItem.cs | 1203 +++++++------ .../MappingRuleViewModelItem.cs | 104 +- ...onthlyBucketExpensesReportViewModelItem.cs | 57 +- .../PartialBucketViewModelItem.cs | 209 ++- .../ItemViewModels/RuleSetViewModelItem.cs | 367 ++-- .../TransactionViewModelItem.cs | 975 ++++++----- .../ViewModels/ReportViewModel.cs | 476 +++-- .../ViewModels/RulesViewModel.cs | 292 ++-- .../ViewModels/TransactionViewModel.cs | 603 ++++--- .../ViewModels/ViewModelBase.cs | 34 +- .../ViewModels/YearMonthSelectorViewModel.cs | 173 +- 52 files changed, 5624 insertions(+), 5791 deletions(-) diff --git a/OpenBudgeteer.Blazor/Dockerfile b/OpenBudgeteer.Blazor/Dockerfile index 895a470..759e07c 100644 --- a/OpenBudgeteer.Blazor/Dockerfile +++ b/OpenBudgeteer.Blazor/Dockerfile @@ -1,11 +1,11 @@ #See https://aka.ms/containerfastmode to understand how Visual Studio uses this Dockerfile to build your images for faster debugging. -FROM mcr.microsoft.com/dotnet/aspnet:5.0-buster-slim AS base +FROM mcr.microsoft.com/dotnet/aspnet:6.0 AS base WORKDIR /app EXPOSE 80 EXPOSE 443 -FROM mcr.microsoft.com/dotnet/sdk:5.0 AS build +FROM mcr.microsoft.com/dotnet/sdk:6.0 AS build WORKDIR /src COPY ["OpenBudgeteer.Blazor/OpenBudgeteer.Blazor.csproj", "OpenBudgeteer.Blazor/"] COPY ["OpenBudgeteer.Core/OpenBudgeteer.Core.csproj", "OpenBudgeteer.Core/"] diff --git a/OpenBudgeteer.Blazor/OpenBudgeteer.Blazor.csproj b/OpenBudgeteer.Blazor/OpenBudgeteer.Blazor.csproj index dbfaf3f..dc005c9 100644 --- a/OpenBudgeteer.Blazor/OpenBudgeteer.Blazor.csproj +++ b/OpenBudgeteer.Blazor/OpenBudgeteer.Blazor.csproj @@ -1,7 +1,7 @@  - net5.0 + net6.0 c146cdfd-f78c-4fb3-8be0-4e15e589371a Linux OpenBudgeteer diff --git a/OpenBudgeteer.Blazor/Program.cs b/OpenBudgeteer.Blazor/Program.cs index 64b1474..6639753 100644 --- a/OpenBudgeteer.Blazor/Program.cs +++ b/OpenBudgeteer.Blazor/Program.cs @@ -1,28 +1,19 @@ -using System; -using System.Collections.Generic; -using System.IO; -using System.Linq; -using System.Threading.Tasks; -using Microsoft.AspNetCore; using Microsoft.AspNetCore.Hosting; -using Microsoft.Extensions.Configuration; using Microsoft.Extensions.Hosting; -using Microsoft.Extensions.Logging; -namespace OpenBudgeteer.Blazor +namespace OpenBudgeteer.Blazor; + +public class Program { - public class Program + public static void Main(string[] args) { - public static void Main(string[] args) - { - CreateHostBuilder(args).Build().Run(); - } - - public static IHostBuilder CreateHostBuilder(string[] args) => - Host.CreateDefaultBuilder(args) - .ConfigureWebHostDefaults(webBuilder => - { - webBuilder.UseStartup(); - }); + CreateHostBuilder(args).Build().Run(); } + + public static IHostBuilder CreateHostBuilder(string[] args) => + Host.CreateDefaultBuilder(args) + .ConfigureWebHostDefaults(webBuilder => + { + webBuilder.UseStartup(); + }); } diff --git a/OpenBudgeteer.Blazor/Startup.cs b/OpenBudgeteer.Blazor/Startup.cs index 100725f..2ca6d23 100644 --- a/OpenBudgeteer.Blazor/Startup.cs +++ b/OpenBudgeteer.Blazor/Startup.cs @@ -1,13 +1,9 @@ using System; -using System.Collections.Generic; using System.Globalization; using System.Linq; using System.Text; -using System.Threading.Tasks; using Microsoft.AspNetCore.Builder; -using Microsoft.AspNetCore.Components; using Microsoft.AspNetCore.Hosting; -using Microsoft.AspNetCore.HttpsPolicy; using Microsoft.EntityFrameworkCore; using Microsoft.Extensions.Configuration; using Microsoft.Extensions.DependencyInjection; @@ -16,100 +12,99 @@ using OpenBudgeteer.Core.ViewModels; using Tewr.Blazor.FileReader; -namespace OpenBudgeteer.Blazor +namespace OpenBudgeteer.Blazor; + +public class Startup { - public class Startup + public Startup(IConfiguration configuration) { - public Startup(IConfiguration configuration) - { - Configuration = configuration; - } + Configuration = configuration; + } - public IConfiguration Configuration { get; } + public IConfiguration Configuration { get; } - // This method gets called by the runtime. Use this method to add services to the container. - // For more information on how to configure your application, visit https://go.microsoft.com/fwlink/?LinkID=398940 - public void ConfigureServices(IServiceCollection services) + // This method gets called by the runtime. Use this method to add services to the container. + // For more information on how to configure your application, visit https://go.microsoft.com/fwlink/?LinkID=398940 + public void ConfigureServices(IServiceCollection services) + { + services.AddRazorPages(); + services.AddServerSideBlazor(); + services.AddFileReaderService(); + services.AddScoped(); + var configurationSection = Configuration.GetSection("Connection"); + var provider = configurationSection?["Provider"]; + string connectionString; + switch (provider) { - services.AddRazorPages(); - services.AddServerSideBlazor(); - services.AddFileReaderService(); - services.AddScoped(); - var configurationSection = Configuration.GetSection("Connection"); - var provider = configurationSection?["Provider"]; - string connectionString; - switch (provider) - { - case "mysql": - connectionString = $"Server={configurationSection?["Server"]};" + - $"Port={configurationSection?["Port"]};" + - $"Database={configurationSection?["Database"]};" + - $"User={configurationSection?["User"]};" + - $"Password={configurationSection?["Password"]}"; - - services.AddDbContext(options => options.UseMySql( - connectionString, - ServerVersion.AutoDetect(connectionString), - b => b.MigrationsAssembly("OpenBudgeteer.Core")), - ServiceLifetime.Transient); + case "mysql": + connectionString = $"Server={configurationSection?["Server"]};" + + $"Port={configurationSection?["Port"]};" + + $"Database={configurationSection?["Database"]};" + + $"User={configurationSection?["User"]};" + + $"Password={configurationSection?["Password"]}"; + + services.AddDbContext(options => options.UseMySql( + connectionString, + ServerVersion.AutoDetect(connectionString), + b => b.MigrationsAssembly("OpenBudgeteer.Core")), + ServiceLifetime.Transient); - // Check on Pending Db Migrations - var mySqlDbContext = new MySqlDatabaseContextFactory().CreateDbContext(Configuration); - if (mySqlDbContext.Database.GetPendingMigrations().Any()) mySqlDbContext.Database.Migrate(); - - break; - case "sqlite": + // Check on Pending Db Migrations + var mySqlDbContext = new MySqlDatabaseContextFactory().CreateDbContext(Configuration); + if (mySqlDbContext.Database.GetPendingMigrations().Any()) mySqlDbContext.Database.Migrate(); + + break; + case "sqlite": #if DEBUG - connectionString = "Data Source=openbudgeteer.db"; + connectionString = "Data Source=openbudgeteer.db"; #else - connectionString = "Data Source=database/openbudgeteer.db"; + connectionString = "Data Source=database/openbudgeteer.db"; #endif - services.AddDbContext(options => options.UseSqlite( - connectionString, - b => b.MigrationsAssembly("OpenBudgeteer.Core")), - ServiceLifetime.Transient); + services.AddDbContext(options => options.UseSqlite( + connectionString, + b => b.MigrationsAssembly("OpenBudgeteer.Core")), + ServiceLifetime.Transient); - // Check on Pending Db Migrations - var sqliteDbContext = new SqliteDatabaseContextFactory().CreateDbContext(connectionString); - if (sqliteDbContext.Database.GetPendingMigrations().Any()) sqliteDbContext.Database.Migrate(); + // Check on Pending Db Migrations + var sqliteDbContext = new SqliteDatabaseContextFactory().CreateDbContext(connectionString); + if (sqliteDbContext.Database.GetPendingMigrations().Any()) sqliteDbContext.Database.Migrate(); - break; - default: - throw new ArgumentOutOfRangeException($"Database provider {provider} not supported"); - } - - Encoding.RegisterProvider(CodePagesEncodingProvider.Instance); // Required to read ANSI Text files + break; + default: + throw new ArgumentOutOfRangeException($"Database provider {provider} not supported"); } + + Encoding.RegisterProvider(CodePagesEncodingProvider.Instance); // Required to read ANSI Text files + } - // This method gets called by the runtime. Use this method to configure the HTTP request pipeline. - public void Configure(IApplicationBuilder app, IWebHostEnvironment env) + // This method gets called by the runtime. Use this method to configure the HTTP request pipeline. + public void Configure(IApplicationBuilder app, IWebHostEnvironment env) + { + if (env.IsDevelopment()) + { + app.UseDeveloperExceptionPage(); + } + else { - if (env.IsDevelopment()) - { - app.UseDeveloperExceptionPage(); - } - else - { - app.UseExceptionHandler("/Error"); - // The default HSTS value is 30 days. You may want to change this for production scenarios, see https://aka.ms/aspnetcore-hsts. - app.UseHsts(); - } + app.UseExceptionHandler("/Error"); + // The default HSTS value is 30 days. You may want to change this for production scenarios, see https://aka.ms/aspnetcore-hsts. + app.UseHsts(); + } - app.UseHttpsRedirection(); - app.UseStaticFiles(); + app.UseHttpsRedirection(); + app.UseStaticFiles(); - app.UseRouting(); + app.UseRouting(); - app.UseEndpoints(endpoints => - { - endpoints.MapBlazorHub(); - endpoints.MapFallbackToPage("/_Host"); - }); + app.UseEndpoints(endpoints => + { + endpoints.MapBlazorHub(); + endpoints.MapFallbackToPage("/_Host"); + }); - // TODO Get Culture from Settings - var cultureInfo = new CultureInfo("de-DE"); - CultureInfo.DefaultThreadCurrentCulture = cultureInfo; - CultureInfo.DefaultThreadCurrentUICulture = cultureInfo; - } + // TODO Get Culture from Settings + var cultureInfo = new CultureInfo("de-DE"); + CultureInfo.DefaultThreadCurrentCulture = cultureInfo; + CultureInfo.DefaultThreadCurrentUICulture = cultureInfo; } } diff --git a/OpenBudgeteer.Blazor/ViewModels/BlazorReportViewModel.cs b/OpenBudgeteer.Blazor/ViewModels/BlazorReportViewModel.cs index c97ea59..8e8c1cb 100644 --- a/OpenBudgeteer.Blazor/ViewModels/BlazorReportViewModel.cs +++ b/OpenBudgeteer.Blazor/ViewModels/BlazorReportViewModel.cs @@ -17,326 +17,325 @@ using OpenBudgeteer.Core.Common.Database; using OpenBudgeteer.Core.ViewModels; -namespace OpenBudgeteer.Blazor.ViewModels +namespace OpenBudgeteer.Blazor.ViewModels; + +public class BlazorReportViewModel : ReportViewModel { - public class BlazorReportViewModel : ReportViewModel + private BarConfig _monthBalancesConfig; + public BarConfig MonthBalancesConfig { - private BarConfig _monthBalancesConfig; - public BarConfig MonthBalancesConfig - { - get => _monthBalancesConfig; - set => Set(ref _monthBalancesConfig, value); - } + get => _monthBalancesConfig; + set => Set(ref _monthBalancesConfig, value); + } - private LineConfig _bankBalancesConfig; - public LineConfig BankBalancesConfig - { - get => _bankBalancesConfig; - set => Set(ref _bankBalancesConfig, value); - } + private LineConfig _bankBalancesConfig; + public LineConfig BankBalancesConfig + { + get => _bankBalancesConfig; + set => Set(ref _bankBalancesConfig, value); + } - private BarConfig _monthIncomeExpensesConfig; - public BarConfig MonthIncomeExpensesConfig - { - get => _monthIncomeExpensesConfig; - set => Set(ref _monthIncomeExpensesConfig, value); - } + private BarConfig _monthIncomeExpensesConfig; + public BarConfig MonthIncomeExpensesConfig + { + get => _monthIncomeExpensesConfig; + set => Set(ref _monthIncomeExpensesConfig, value); + } - private BarConfig _yearIncomeExpensesConfig; - public BarConfig YearIncomeExpensesConfig - { - get => _yearIncomeExpensesConfig; - set => Set(ref _yearIncomeExpensesConfig, value); - } + private BarConfig _yearIncomeExpensesConfig; + public BarConfig YearIncomeExpensesConfig + { + get => _yearIncomeExpensesConfig; + set => Set(ref _yearIncomeExpensesConfig, value); + } - private ObservableCollection> _monthBucketExpensesConfigs; - public ObservableCollection> MonthBucketExpensesConfigs - { - get => _monthBucketExpensesConfigs; - set => Set(ref _monthBucketExpensesConfigs, value); - } + private ObservableCollection> _monthBucketExpensesConfigs; + public ObservableCollection> MonthBucketExpensesConfigs + { + get => _monthBucketExpensesConfigs; + set => Set(ref _monthBucketExpensesConfigs, value); + } - protected BarConfig DefaultBarConfig => - new BarConfig() + protected BarConfig DefaultBarConfig => + new BarConfig() + { + Options = new BarOptions { - Options = new BarOptions + Title = new OptionsTitle { - Title = new OptionsTitle - { - Display = false - }, - Animation = new ArcAnimation - { - AnimateRotate = true, - AnimateScale = true - }, - Legend = new Legend - { - Display = false - } + Display = false + }, + Animation = new ArcAnimation + { + AnimateRotate = true, + AnimateScale = true + }, + Legend = new Legend + { + Display = false } - }; + } + }; - protected BarConfig DefaultBucketExpensesBarConfig + protected BarConfig DefaultBucketExpensesBarConfig + { + get { - get + var result = this.DefaultBarConfig; + result.Options.Scales = new BarScales { - var result = this.DefaultBarConfig; - result.Options.Scales = new BarScales + YAxes = new List { - YAxes = new List + new BarLinearCartesianAxis { - new BarLinearCartesianAxis + Ticks = new LinearCartesianTicks { - Ticks = new LinearCartesianTicks - { - Min = 0 - } + Min = 0 } } - }; - return result; - } + } + }; + return result; } - - protected LineConfig DefaultTImeLineConfig => - new LineConfig + } + + protected LineConfig DefaultTImeLineConfig => + new LineConfig + { + Options = new LineOptions { - Options = new LineOptions + Title = new OptionsTitle { - Title = new OptionsTitle - { - Display = false - }, - Legend = new Legend - { - Display = false - }, - Tooltips = new Tooltips - { - Mode = InteractionMode.Nearest, - Intersect = false - }, - Scales = new Scales + Display = false + }, + Legend = new Legend + { + Display = false + }, + Tooltips = new Tooltips + { + Mode = InteractionMode.Nearest, + Intersect = false + }, + Scales = new Scales + { + xAxes = new List { - xAxes = new List + new TimeAxis { - new TimeAxis + Distribution = TimeDistribution.Linear, + Ticks = new TimeTicks { - Distribution = TimeDistribution.Linear, - Ticks = new TimeTicks - { - Source = TickSource.Data - }, - Time = new TimeOptions - { - Unit = TimeMeasurement.Month, - Round = TimeMeasurement.Month, - TooltipFormat = "MM.YYYY", - DisplayFormats = TimeDisplayFormats.DE_CH - } + Source = TickSource.Data + }, + Time = new TimeOptions + { + Unit = TimeMeasurement.Month, + Round = TimeMeasurement.Month, + TooltipFormat = "MM.YYYY", + DisplayFormats = TimeDisplayFormats.DE_CH } } - }, - Hover = new LineOptionsHover - { - Intersect = true, - Mode = InteractionMode.Y } + }, + Hover = new LineOptionsHover + { + Intersect = true, + Mode = InteractionMode.Y } - }; + } + }; - public BlazorReportViewModel(DbContextOptions dbOptions) : base(dbOptions) - { - MonthBalancesConfig = new BarConfig(); - BankBalancesConfig = new LineConfig(); - MonthIncomeExpensesConfig = new BarConfig(); - YearIncomeExpensesConfig = new BarConfig(); - MonthBucketExpensesConfigs = new ObservableCollection>(); - } + public BlazorReportViewModel(DbContextOptions dbOptions) : base(dbOptions) + { + MonthBalancesConfig = new BarConfig(); + BankBalancesConfig = new LineConfig(); + MonthIncomeExpensesConfig = new BarConfig(); + YearIncomeExpensesConfig = new BarConfig(); + MonthBucketExpensesConfigs = new ObservableCollection>(); + } - public async Task LoadDataAsync() + public async Task LoadDataAsync() + { + var loadTasks = new List() { - var loadTasks = new List() - { - LoadMonthBalancesReportAsync(), - LoadMonthIncomeExpensesReportAsync(), - LoadYearIncomeExpensesReportAsync(), - LoadBankBalancesReportAsync(), - LoadMonthExpensesBucketReportAsync() - }; - await Task.WhenAll(loadTasks); - } + LoadMonthBalancesReportAsync(), + LoadMonthIncomeExpensesReportAsync(), + LoadYearIncomeExpensesReportAsync(), + LoadBankBalancesReportAsync(), + LoadMonthExpensesBucketReportAsync() + }; + await Task.WhenAll(loadTasks); + } - private async Task LoadMonthBalancesReportAsync() - { - MonthBalancesConfig = DefaultBarConfig; - MonthBalancesConfig.Options.Title.Text = "Month Balances"; + private async Task LoadMonthBalancesReportAsync() + { + MonthBalancesConfig = DefaultBarConfig; + MonthBalancesConfig.Options.Title.Text = "Month Balances"; - var backgroundColors = new List(); - var hoverColors = new List(); - var data = new List(); + var backgroundColors = new List(); + var hoverColors = new List(); + var data = new List(); - var monthBalanceData = await LoadMonthBalancesAsync(); - foreach (var balanceData in monthBalanceData) + var monthBalanceData = await LoadMonthBalancesAsync(); + foreach (var balanceData in monthBalanceData) + { + data.Add(balanceData.Item2); + MonthBalancesConfig.Data.Labels.Add(balanceData.Item1.ToString("MM.yyyy")); + if (balanceData.Item2 < 0) { - data.Add(balanceData.Item2); - MonthBalancesConfig.Data.Labels.Add(balanceData.Item1.ToString("MM.yyyy")); - if (balanceData.Item2 < 0) - { - backgroundColors.Add(ColorUtil.FromDrawingColor(Color.DarkRed)); - hoverColors.Add(ColorUtil.FromDrawingColor(Color.LightCoral)); - } - else - { - backgroundColors.Add(ColorUtil.FromDrawingColor(Color.Green)); - hoverColors.Add(ColorUtil.FromDrawingColor(Color.LightGreen)); - } + backgroundColors.Add(ColorUtil.FromDrawingColor(Color.DarkRed)); + hoverColors.Add(ColorUtil.FromDrawingColor(Color.LightCoral)); } - - var barDataSet = new BarDataset + else { - BackgroundColor = backgroundColors.ToArray(), - BorderWidth = 0, - HoverBackgroundColor = hoverColors.ToArray(), - HoverBorderWidth = 0 - }; - - barDataSet.AddRange(data); - - MonthBalancesConfig.Data.Datasets.Add(barDataSet); + backgroundColors.Add(ColorUtil.FromDrawingColor(Color.Green)); + hoverColors.Add(ColorUtil.FromDrawingColor(Color.LightGreen)); + } } - private async Task LoadMonthIncomeExpensesReportAsync() + var barDataSet = new BarDataset { - MonthIncomeExpensesConfig = DefaultBarConfig; - MonthIncomeExpensesConfig.Options.Title.Text = "Income & Expenses per Month"; + BackgroundColor = backgroundColors.ToArray(), + BorderWidth = 0, + HoverBackgroundColor = hoverColors.ToArray(), + HoverBorderWidth = 0 + }; - var incomeResults = new List(); - var expensesResults = new List(); - - foreach (var (month, income, expenses) in await LoadMonthIncomeExpensesAsync()) - { - incomeResults.Add(income); - expensesResults.Add(expenses); - MonthIncomeExpensesConfig.Data.Labels.Add(month.ToString("MM.yyyy")); - } + barDataSet.AddRange(data); - var incomeBarDataSet = new BarDataset - { - BackgroundColor = ColorUtil.FromDrawingColor(Color.Green), - BorderWidth = 0, - HoverBackgroundColor = ColorUtil.FromDrawingColor(Color.LightGreen), - HoverBorderWidth = 0 - }; - - var expensesBarDataSet = new BarDataset - { - BackgroundColor = ColorUtil.FromDrawingColor(Color.DarkRed), - BorderWidth = 0, - HoverBackgroundColor = ColorUtil.FromDrawingColor(Color.LightCoral), - HoverBorderWidth = 0 - }; + MonthBalancesConfig.Data.Datasets.Add(barDataSet); + } - incomeBarDataSet.AddRange(incomeResults); - expensesBarDataSet.AddRange(expensesResults); + private async Task LoadMonthIncomeExpensesReportAsync() + { + MonthIncomeExpensesConfig = DefaultBarConfig; + MonthIncomeExpensesConfig.Options.Title.Text = "Income & Expenses per Month"; - MonthIncomeExpensesConfig.Data.Datasets.Add(incomeBarDataSet); - MonthIncomeExpensesConfig.Data.Datasets.Add(expensesBarDataSet); + var incomeResults = new List(); + var expensesResults = new List(); + + foreach (var (month, income, expenses) in await LoadMonthIncomeExpensesAsync()) + { + incomeResults.Add(income); + expensesResults.Add(expenses); + MonthIncomeExpensesConfig.Data.Labels.Add(month.ToString("MM.yyyy")); } - private async Task LoadYearIncomeExpensesReportAsync() + var incomeBarDataSet = new BarDataset { - YearIncomeExpensesConfig = DefaultBarConfig; - YearIncomeExpensesConfig.Options.Title.Text = "Income & Expenses per Year"; + BackgroundColor = ColorUtil.FromDrawingColor(Color.Green), + BorderWidth = 0, + HoverBackgroundColor = ColorUtil.FromDrawingColor(Color.LightGreen), + HoverBorderWidth = 0 + }; - var incomeResults = new List(); - var expensesResults = new List(); + var expensesBarDataSet = new BarDataset + { + BackgroundColor = ColorUtil.FromDrawingColor(Color.DarkRed), + BorderWidth = 0, + HoverBackgroundColor = ColorUtil.FromDrawingColor(Color.LightCoral), + HoverBorderWidth = 0 + }; - foreach (var (month, income, expenses) in await LoadYearIncomeExpensesAsync()) - { - incomeResults.Add(income); - expensesResults.Add(expenses); - YearIncomeExpensesConfig.Data.Labels.Add(month.ToString("yyyy")); - } + incomeBarDataSet.AddRange(incomeResults); + expensesBarDataSet.AddRange(expensesResults); - var incomeBarDataSet = new BarDataset - { - BackgroundColor = ColorUtil.FromDrawingColor(Color.Green), - BorderWidth = 0, - HoverBackgroundColor = ColorUtil.FromDrawingColor(Color.LightGreen), - HoverBorderWidth = 0 - }; + MonthIncomeExpensesConfig.Data.Datasets.Add(incomeBarDataSet); + MonthIncomeExpensesConfig.Data.Datasets.Add(expensesBarDataSet); + } - var expensesBarDataSet = new BarDataset - { - BackgroundColor = ColorUtil.FromDrawingColor(Color.DarkRed), - BorderWidth = 0, - HoverBackgroundColor = ColorUtil.FromDrawingColor(Color.LightCoral), - HoverBorderWidth = 0 - }; + private async Task LoadYearIncomeExpensesReportAsync() + { + YearIncomeExpensesConfig = DefaultBarConfig; + YearIncomeExpensesConfig.Options.Title.Text = "Income & Expenses per Year"; - incomeBarDataSet.AddRange(incomeResults); - expensesBarDataSet.AddRange(expensesResults); + var incomeResults = new List(); + var expensesResults = new List(); - YearIncomeExpensesConfig.Data.Datasets.Add(incomeBarDataSet); - YearIncomeExpensesConfig.Data.Datasets.Add(expensesBarDataSet); + foreach (var (month, income, expenses) in await LoadYearIncomeExpensesAsync()) + { + incomeResults.Add(income); + expensesResults.Add(expenses); + YearIncomeExpensesConfig.Data.Labels.Add(month.ToString("yyyy")); } - private async Task LoadBankBalancesReportAsync() + var incomeBarDataSet = new BarDataset { - BankBalancesConfig = DefaultTImeLineConfig; - BankBalancesConfig.Options.Title.Text = "Bank Balances"; + BackgroundColor = ColorUtil.FromDrawingColor(Color.Green), + BorderWidth = 0, + HoverBackgroundColor = ColorUtil.FromDrawingColor(Color.LightGreen), + HoverBorderWidth = 0 + }; - var lineDataSet = new LineDataset> - { - BackgroundColor = ColorUtil.FromDrawingColor(Color.Green), - BorderColor = ColorUtil.FromDrawingColor(Color.LightGreen), - Label = "Bank Balance", - Fill = true, - BorderWidth = 2, - PointRadius = 3, - PointBorderWidth = 1, - SteppedLine = SteppedLine.False - }; + var expensesBarDataSet = new BarDataset + { + BackgroundColor = ColorUtil.FromDrawingColor(Color.DarkRed), + BorderWidth = 0, + HoverBackgroundColor = ColorUtil.FromDrawingColor(Color.LightCoral), + HoverBorderWidth = 0 + }; - foreach (var (month, balance) in await LoadBankBalancesAsync()) - { - lineDataSet.Add(new TimeTuple(new Moment(month), Convert.ToDouble(balance))); - } + incomeBarDataSet.AddRange(incomeResults); + expensesBarDataSet.AddRange(expensesResults); - BankBalancesConfig.Data.Datasets.Add(lineDataSet); - } + YearIncomeExpensesConfig.Data.Datasets.Add(incomeBarDataSet); + YearIncomeExpensesConfig.Data.Datasets.Add(expensesBarDataSet); + } + + private async Task LoadBankBalancesReportAsync() + { + BankBalancesConfig = DefaultTImeLineConfig; + BankBalancesConfig.Options.Title.Text = "Bank Balances"; - private async Task LoadMonthExpensesBucketReportAsync() + var lineDataSet = new LineDataset> { - MonthBucketExpensesConfigs.Clear(); - foreach (var result in await LoadMonthExpensesBucketAsync()) - { - var newConfig = DefaultBucketExpensesBarConfig; - newConfig.Options.Title.Display = false; + BackgroundColor = ColorUtil.FromDrawingColor(Color.Green), + BorderColor = ColorUtil.FromDrawingColor(Color.LightGreen), + Label = "Bank Balance", + Fill = true, + BorderWidth = 2, + PointRadius = 3, + PointBorderWidth = 1, + SteppedLine = SteppedLine.False + }; + + foreach (var (month, balance) in await LoadBankBalancesAsync()) + { + lineDataSet.Add(new TimeTuple(new Moment(month), Convert.ToDouble(balance))); + } - var expensesResults = new List(); - foreach (var monthlyResult in result.MonthlyResults) - { - expensesResults.Add(monthlyResult.Item2); - newConfig.Data.Labels.Add(monthlyResult.Item1.ToString("MM.yyyy")); - } + BankBalancesConfig.Data.Datasets.Add(lineDataSet); + } - var newDataSet = new BarDataset() - { - BackgroundColor = ColorUtil.FromDrawingColor(Color.DarkRed), - BorderWidth = 0, - HoverBackgroundColor = ColorUtil.FromDrawingColor(Color.LightCoral), - HoverBorderWidth = 0 - }; - newDataSet.AddRange(expensesResults); - newConfig.Data.Datasets.Add(newDataSet); - - MonthBucketExpensesConfigs.Add(new Tuple( - result.BucketName, - newConfig)); + private async Task LoadMonthExpensesBucketReportAsync() + { + MonthBucketExpensesConfigs.Clear(); + foreach (var result in await LoadMonthExpensesBucketAsync()) + { + var newConfig = DefaultBucketExpensesBarConfig; + newConfig.Options.Title.Display = false; + + var expensesResults = new List(); + foreach (var monthlyResult in result.MonthlyResults) + { + expensesResults.Add(monthlyResult.Item2); + newConfig.Data.Labels.Add(monthlyResult.Item1.ToString("MM.yyyy")); } + + var newDataSet = new BarDataset() + { + BackgroundColor = ColorUtil.FromDrawingColor(Color.DarkRed), + BorderWidth = 0, + HoverBackgroundColor = ColorUtil.FromDrawingColor(Color.LightCoral), + HoverBorderWidth = 0 + }; + newDataSet.AddRange(expensesResults); + newConfig.Data.Datasets.Add(newDataSet); + + MonthBucketExpensesConfigs.Add(new Tuple( + result.BucketName, + newConfig)); } } } diff --git a/OpenBudgeteer.Blazor/appsettings.json b/OpenBudgeteer.Blazor/appsettings.json index 788081e..4691fa9 100644 --- a/OpenBudgeteer.Blazor/appsettings.json +++ b/OpenBudgeteer.Blazor/appsettings.json @@ -2,7 +2,7 @@ "Connection": { "Provider" : "mysql", "Database": "openbudgeteer-dev", - "Server": "host.docker.internal", + "Server": "127.0.0.1", "Port": "3306", "User": "openbudgeteer-dev", "Password": "openbudgeteer-dev" diff --git a/OpenBudgeteer.Core.Test/DbConnector.cs b/OpenBudgeteer.Core.Test/DbConnector.cs index dd7bd95..3712e88 100644 --- a/OpenBudgeteer.Core.Test/DbConnector.cs +++ b/OpenBudgeteer.Core.Test/DbConnector.cs @@ -2,48 +2,47 @@ using Microsoft.EntityFrameworkCore; using OpenBudgeteer.Core.Common.Database; -namespace OpenBudgeteer.Core.Test +namespace OpenBudgeteer.Core.Test; + +public class DbConnector { - public class DbConnector + public static DbContextOptions GetDbContextOptions(string dbName) { - public static DbContextOptions GetDbContextOptions(string dbName) - { - //var connectionString = "Server=cl4p-tp;" + - // "Port=3306;" + - // "Database=openbudgeteer-test;" + - // "User=openbudgeteer-test;" + - // "Password=openbudgeteer-test"; - //return new DbContextOptionsBuilder() - // .UseMySql(connectionString) - // .Options; + //var connectionString = "Server=cl4p-tp;" + + // "Port=3306;" + + // "Database=openbudgeteer-test;" + + // "User=openbudgeteer-test;" + + // "Password=openbudgeteer-test"; + //return new DbContextOptionsBuilder() + // .UseMySql(connectionString) + // .Options; - var connectionString = $"Data Source={dbName}.db"; + var connectionString = $"Data Source={dbName}.db"; - //Check on Pending Db Migrations - var sqliteDbContext = new SqliteDatabaseContextFactory().CreateDbContext(connectionString); - if (sqliteDbContext.Database.GetPendingMigrations().Any()) - sqliteDbContext.Database.Migrate(); + //Check on Pending Db Migrations + var sqliteDbContext = new SqliteDatabaseContextFactory().CreateDbContext(connectionString); + if (sqliteDbContext.Database.GetPendingMigrations().Any()) + sqliteDbContext.Database.Migrate(); - return new DbContextOptionsBuilder() - .UseSqlite(connectionString) - .Options; - } + return new DbContextOptionsBuilder() + .UseSqlite(connectionString) + .Options; + } - public static void CleanupDatabase(string dbName) + public static void CleanupDatabase(string dbName) + { + using (var dbContext = new DatabaseContext(GetDbContextOptions(dbName))) { - using (var dbContext = new DatabaseContext(GetDbContextOptions(dbName))) - { - dbContext.DeleteAccounts(dbContext.Account); - dbContext.DeleteBankTransactions(dbContext.BankTransaction); - dbContext.DeleteBuckets(dbContext.Bucket); - dbContext.DeleteBucketGroups(dbContext.BucketGroup); - dbContext.DeleteBucketMovements(dbContext.BucketMovement); - dbContext.DeleteBucketRuleSets(dbContext.BucketRuleSet); - dbContext.DeleteBucketVersions(dbContext.BucketVersion); - dbContext.DeleteBudgetedTransactions(dbContext.BudgetedTransaction); - dbContext.DeleteImportProfiles(dbContext.ImportProfile); - dbContext.DeleteMappingRules(dbContext.MappingRule); - } + dbContext.DeleteAccounts(dbContext.Account); + dbContext.DeleteBankTransactions(dbContext.BankTransaction); + dbContext.DeleteBuckets(dbContext.Bucket); + dbContext.DeleteBucketGroups(dbContext.BucketGroup); + dbContext.DeleteBucketMovements(dbContext.BucketMovement); + dbContext.DeleteBucketRuleSets(dbContext.BucketRuleSet); + dbContext.DeleteBucketVersions(dbContext.BucketVersion); + dbContext.DeleteBudgetedTransactions(dbContext.BudgetedTransaction); + dbContext.DeleteImportProfiles(dbContext.ImportProfile); + dbContext.DeleteMappingRules(dbContext.MappingRule); } } } diff --git a/OpenBudgeteer.Core.Test/OpenBudgeteer.Core.Test.csproj b/OpenBudgeteer.Core.Test/OpenBudgeteer.Core.Test.csproj index d6a03d3..a026f00 100644 --- a/OpenBudgeteer.Core.Test/OpenBudgeteer.Core.Test.csproj +++ b/OpenBudgeteer.Core.Test/OpenBudgeteer.Core.Test.csproj @@ -4,9 +4,9 @@ false - 9 + 10 - net5.0 + net6.0 diff --git a/OpenBudgeteer.Core.Test/ViewModelTest/AccountViewModelIsolatedTest.cs b/OpenBudgeteer.Core.Test/ViewModelTest/AccountViewModelIsolatedTest.cs index 000a66a..73a591d 100644 --- a/OpenBudgeteer.Core.Test/ViewModelTest/AccountViewModelIsolatedTest.cs +++ b/OpenBudgeteer.Core.Test/ViewModelTest/AccountViewModelIsolatedTest.cs @@ -5,43 +5,42 @@ using OpenBudgeteer.Core.ViewModels; using Xunit; -namespace OpenBudgeteer.Core.Test.ViewModelTest +namespace OpenBudgeteer.Core.Test.ViewModelTest; + +[CollectionDefinition("AccountViewModelIsolatedTest", DisableParallelization = true)] +public class AccountViewModelIsolatedTest { - [CollectionDefinition("AccountViewModelIsolatedTest", DisableParallelization = true)] - public class AccountViewModelIsolatedTest - { - private readonly DbContextOptions _dbOptions; + private readonly DbContextOptions _dbOptions; - public AccountViewModelIsolatedTest() - { - _dbOptions = DbConnector.GetDbContextOptions(nameof(AccountViewModelIsolatedTest)); - } + public AccountViewModelIsolatedTest() + { + _dbOptions = DbConnector.GetDbContextOptions(nameof(AccountViewModelIsolatedTest)); + } - [Fact] - public void LoadData_CheckNameAndLoadOnlyActiveAccounts() + [Fact] + public void LoadData_CheckNameAndLoadOnlyActiveAccounts() + { + DbConnector.CleanupDatabase(nameof(AccountViewModelIsolatedTest)); + + using (var dbContext = new DatabaseContext(_dbOptions)) { - DbConnector.CleanupDatabase(nameof(AccountViewModelIsolatedTest)); - - using (var dbContext = new DatabaseContext(_dbOptions)) + dbContext.CreateAccounts(new[] { - dbContext.CreateAccounts(new[] - { - new Account() {Name = "Test Account1", IsActive = 1}, - new Account() {Name = "Test Account2", IsActive = 1}, - new Account() {Name = "Test Account3", IsActive = 0} - }); - } + new Account() {Name = "Test Account1", IsActive = 1}, + new Account() {Name = "Test Account2", IsActive = 1}, + new Account() {Name = "Test Account3", IsActive = 0} + }); + } - var viewModel = new AccountViewModel(_dbOptions); - viewModel.LoadData(); + var viewModel = new AccountViewModel(_dbOptions); + viewModel.LoadData(); - Assert.Equal(2, viewModel.Accounts.Count); + Assert.Equal(2, viewModel.Accounts.Count); - var testItem1 = viewModel.Accounts.ElementAt(0); - var testItem2 = viewModel.Accounts.ElementAt(1); + var testItem1 = viewModel.Accounts.ElementAt(0); + var testItem2 = viewModel.Accounts.ElementAt(1); - Assert.Equal("Test Account1", testItem1.Account.Name); - Assert.Equal("Test Account2", testItem2.Account.Name); - } + Assert.Equal("Test Account1", testItem1.Account.Name); + Assert.Equal("Test Account2", testItem2.Account.Name); } -} \ No newline at end of file +} diff --git a/OpenBudgeteer.Core.Test/ViewModelTest/AccountViewModelTest.cs b/OpenBudgeteer.Core.Test/ViewModelTest/AccountViewModelTest.cs index 98e485e..3e1c126 100644 --- a/OpenBudgeteer.Core.Test/ViewModelTest/AccountViewModelTest.cs +++ b/OpenBudgeteer.Core.Test/ViewModelTest/AccountViewModelTest.cs @@ -1,6 +1,5 @@ using System; using System.Collections.Generic; -using System.Diagnostics.Contracts; using System.Linq; using Microsoft.EntityFrameworkCore; using OpenBudgeteer.Core.Common.Database; @@ -8,64 +7,63 @@ using OpenBudgeteer.Core.ViewModels; using Xunit; -namespace OpenBudgeteer.Core.Test.ViewModelTest +namespace OpenBudgeteer.Core.Test.ViewModelTest; + +public class AccountViewModelTest { - public class AccountViewModelTest - { - private readonly DbContextOptions _dbOptions; + private readonly DbContextOptions _dbOptions; - public AccountViewModelTest() - { - _dbOptions = DbConnector.GetDbContextOptions(nameof(AccountViewModelTest)); - } + public AccountViewModelTest() + { + _dbOptions = DbConnector.GetDbContextOptions(nameof(AccountViewModelTest)); + } - public static IEnumerable TestData_LoadData_CheckTransactionCalculations + public static IEnumerable TestData_LoadData_CheckTransactionCalculations + { + get { - get + return new[] { - return new[] - { - new object[] {new List {12.34m, -12.34m, 12.34m}, 12.34m, 24.68m, -12.34m}, - new object[] {new List {0}, 0, 0, 0} - }; - } + new object[] {new List {12.34m, -12.34m, 12.34m}, 12.34m, 24.68m, -12.34m}, + new object[] {new List {0}, 0, 0, 0} + }; } - - [Theory] - [MemberData(nameof(TestData_LoadData_CheckTransactionCalculations))] - public void LoadData_CheckTransactionCalculations( - List transactionAmounts, - decimal expectedBalance, - decimal expectedIn, - decimal expectedOut) + } + + [Theory] + [MemberData(nameof(TestData_LoadData_CheckTransactionCalculations))] + public void LoadData_CheckTransactionCalculations( + List transactionAmounts, + decimal expectedBalance, + decimal expectedIn, + decimal expectedOut) + { + using (var dbContext = new DatabaseContext(_dbOptions)) { - using (var dbContext = new DatabaseContext(_dbOptions)) - { - var testAccount = new Account() {Name = "Test Account", IsActive = 1}; - dbContext.CreateAccount(testAccount); - - foreach (var transactionAmount in transactionAmounts) - { - dbContext.CreateBankTransaction( - new BankTransaction() - { - AccountId = testAccount.AccountId, - TransactionDate = DateTime.Now, - Amount = transactionAmount - } - ); - } - - var viewModel = new AccountViewModel(_dbOptions); - viewModel.LoadData(); - var testItem1 = viewModel.Accounts - .FirstOrDefault(i => i.Account.AccountId == testAccount.AccountId); + var testAccount = new Account() {Name = "Test Account", IsActive = 1}; + dbContext.CreateAccount(testAccount); - Assert.NotNull(testItem1); - Assert.Equal(expectedBalance, testItem1.Balance); - Assert.Equal(expectedIn, testItem1.In); - Assert.Equal(expectedOut, testItem1.Out); + foreach (var transactionAmount in transactionAmounts) + { + dbContext.CreateBankTransaction( + new BankTransaction() + { + AccountId = testAccount.AccountId, + TransactionDate = DateTime.Now, + Amount = transactionAmount + } + ); } + + var viewModel = new AccountViewModel(_dbOptions); + viewModel.LoadData(); + var testItem1 = viewModel.Accounts + .FirstOrDefault(i => i.Account.AccountId == testAccount.AccountId); + + Assert.NotNull(testItem1); + Assert.Equal(expectedBalance, testItem1.Balance); + Assert.Equal(expectedIn, testItem1.In); + Assert.Equal(expectedOut, testItem1.Out); } } } diff --git a/OpenBudgeteer.Core.Test/ViewModelTest/BucketViewModelIsolatedTest.cs b/OpenBudgeteer.Core.Test/ViewModelTest/BucketViewModelIsolatedTest.cs index 9e4f2bb..73ac163 100644 --- a/OpenBudgeteer.Core.Test/ViewModelTest/BucketViewModelIsolatedTest.cs +++ b/OpenBudgeteer.Core.Test/ViewModelTest/BucketViewModelIsolatedTest.cs @@ -6,137 +6,136 @@ using OpenBudgeteer.Core.ViewModels; using Xunit; -namespace OpenBudgeteer.Core.Test.ViewModelTest +namespace OpenBudgeteer.Core.Test.ViewModelTest; + +[CollectionDefinition("BucketViewModelIsolatedTest", DisableParallelization = true)] +public class BucketViewModelIsolatedTest { - [CollectionDefinition("BucketViewModelIsolatedTest", DisableParallelization = true)] - public class BucketViewModelIsolatedTest - { - private readonly DbContextOptions _dbOptions; + private readonly DbContextOptions _dbOptions; - public BucketViewModelIsolatedTest() - { - _dbOptions = DbConnector.GetDbContextOptions(nameof(BucketViewModelIsolatedTest)); - } + public BucketViewModelIsolatedTest() + { + _dbOptions = DbConnector.GetDbContextOptions(nameof(BucketViewModelIsolatedTest)); + } + + [Fact] + public async Task LoadDataAsync_CheckBucketGroupsNamesAndPositions() + { + DbConnector.CleanupDatabase(nameof(BucketViewModelIsolatedTest)); - [Fact] - public async Task LoadDataAsync_CheckBucketGroupsNamesAndPositions() + using (var dbContext = new DatabaseContext(_dbOptions)) { - DbConnector.CleanupDatabase(nameof(BucketViewModelIsolatedTest)); - - using (var dbContext = new DatabaseContext(_dbOptions)) + dbContext.CreateBucketGroups(new[] { - dbContext.CreateBucketGroups(new[] - { - new BucketGroup() { Name = "Bucket Group 1", Position = 1}, - new BucketGroup() { Name = "Bucket Group 2", Position = 3}, - new BucketGroup() { Name = "Bucket Group 3", Position = 2} - }); - } + new BucketGroup() { Name = "Bucket Group 1", Position = 1}, + new BucketGroup() { Name = "Bucket Group 2", Position = 3}, + new BucketGroup() { Name = "Bucket Group 3", Position = 2} + }); + } - var monthSelectorViewModel = new YearMonthSelectorViewModel(); - var viewModel = new BucketViewModel(_dbOptions, monthSelectorViewModel); - await viewModel.LoadDataAsync(); + var monthSelectorViewModel = new YearMonthSelectorViewModel(); + var viewModel = new BucketViewModel(_dbOptions, monthSelectorViewModel); + await viewModel.LoadDataAsync(); - Assert.Equal(3, viewModel.BucketGroups.Count); - Assert.Equal("Bucket Group 1", viewModel.BucketGroups.ElementAt(0).BucketGroup.Name); - Assert.Equal("Bucket Group 3", viewModel.BucketGroups.ElementAt(1).BucketGroup.Name); - Assert.Equal("Bucket Group 2", viewModel.BucketGroups.ElementAt(2).BucketGroup.Name); + Assert.Equal(3, viewModel.BucketGroups.Count); + Assert.Equal("Bucket Group 1", viewModel.BucketGroups.ElementAt(0).BucketGroup.Name); + Assert.Equal("Bucket Group 3", viewModel.BucketGroups.ElementAt(1).BucketGroup.Name); + Assert.Equal("Bucket Group 2", viewModel.BucketGroups.ElementAt(2).BucketGroup.Name); - Assert.Equal(1, viewModel.BucketGroups.ElementAt(0).BucketGroup.Position); - Assert.Equal(2, viewModel.BucketGroups.ElementAt(1).BucketGroup.Position); - Assert.Equal(3, viewModel.BucketGroups.ElementAt(2).BucketGroup.Position); - } + Assert.Equal(1, viewModel.BucketGroups.ElementAt(0).BucketGroup.Position); + Assert.Equal(2, viewModel.BucketGroups.ElementAt(1).BucketGroup.Position); + Assert.Equal(3, viewModel.BucketGroups.ElementAt(2).BucketGroup.Position); + } + + [Fact] + public async Task CreateGroup_CheckGroupCreationAndPositions() + { + DbConnector.CleanupDatabase(nameof(BucketViewModelIsolatedTest)); - [Fact] - public async Task CreateGroup_CheckGroupCreationAndPositions() + using (var dbContext = new DatabaseContext(_dbOptions)) { - DbConnector.CleanupDatabase(nameof(BucketViewModelIsolatedTest)); - - using (var dbContext = new DatabaseContext(_dbOptions)) + dbContext.CreateBucketGroups(new[] { - dbContext.CreateBucketGroups(new[] - { - new BucketGroup() { Name = "Bucket Group 1", Position = 1 }, - new BucketGroup() { Name = "Bucket Group 2", Position = 3 }, - new BucketGroup() { Name = "Bucket Group 3", Position = 2 } - }); - } + new BucketGroup() { Name = "Bucket Group 1", Position = 1 }, + new BucketGroup() { Name = "Bucket Group 2", Position = 3 }, + new BucketGroup() { Name = "Bucket Group 3", Position = 2 } + }); + } - var monthSelectorViewModel = new YearMonthSelectorViewModel(); - var viewModel = new BucketViewModel(_dbOptions, monthSelectorViewModel); - await viewModel.LoadDataAsync(); + var monthSelectorViewModel = new YearMonthSelectorViewModel(); + var viewModel = new BucketViewModel(_dbOptions, monthSelectorViewModel); + await viewModel.LoadDataAsync(); - var result = viewModel.CreateGroup(); - - Assert.True(result.IsSuccessful); - Assert.Equal(4, viewModel.BucketGroups.Count); - Assert.Equal("New Bucket Group", viewModel.BucketGroups.ElementAt(0).BucketGroup.Name); - Assert.Equal("Bucket Group 1", viewModel.BucketGroups.ElementAt(1).BucketGroup.Name); - Assert.Equal("Bucket Group 3", viewModel.BucketGroups.ElementAt(2).BucketGroup.Name); - Assert.Equal("Bucket Group 2", viewModel.BucketGroups.ElementAt(3).BucketGroup.Name); - Assert.Equal(1, viewModel.BucketGroups.ElementAt(0).BucketGroup.Position); - Assert.Equal(2, viewModel.BucketGroups.ElementAt(1).BucketGroup.Position); - Assert.Equal(3, viewModel.BucketGroups.ElementAt(2).BucketGroup.Position); - Assert.Equal(4, viewModel.BucketGroups.ElementAt(3).BucketGroup.Position); - Assert.True(viewModel.BucketGroups.First().InModification); - - // Reload ViewModel to see if changes has been also reflected onto the database - await viewModel.LoadDataAsync(); + var result = viewModel.CreateGroup(); + + Assert.True(result.IsSuccessful); + Assert.Equal(4, viewModel.BucketGroups.Count); + Assert.Equal("New Bucket Group", viewModel.BucketGroups.ElementAt(0).BucketGroup.Name); + Assert.Equal("Bucket Group 1", viewModel.BucketGroups.ElementAt(1).BucketGroup.Name); + Assert.Equal("Bucket Group 3", viewModel.BucketGroups.ElementAt(2).BucketGroup.Name); + Assert.Equal("Bucket Group 2", viewModel.BucketGroups.ElementAt(3).BucketGroup.Name); + Assert.Equal(1, viewModel.BucketGroups.ElementAt(0).BucketGroup.Position); + Assert.Equal(2, viewModel.BucketGroups.ElementAt(1).BucketGroup.Position); + Assert.Equal(3, viewModel.BucketGroups.ElementAt(2).BucketGroup.Position); + Assert.Equal(4, viewModel.BucketGroups.ElementAt(3).BucketGroup.Position); + Assert.True(viewModel.BucketGroups.First().InModification); + + // Reload ViewModel to see if changes has been also reflected onto the database + await viewModel.LoadDataAsync(); - Assert.True(result.IsSuccessful); - Assert.Equal(4, viewModel.BucketGroups.Count); - Assert.Equal("New Bucket Group", viewModel.BucketGroups.ElementAt(0).BucketGroup.Name); - Assert.Equal("Bucket Group 1", viewModel.BucketGroups.ElementAt(1).BucketGroup.Name); - Assert.Equal("Bucket Group 3", viewModel.BucketGroups.ElementAt(2).BucketGroup.Name); - Assert.Equal("Bucket Group 2", viewModel.BucketGroups.ElementAt(3).BucketGroup.Name); - Assert.Equal(1, viewModel.BucketGroups.ElementAt(0).BucketGroup.Position); - Assert.Equal(2, viewModel.BucketGroups.ElementAt(1).BucketGroup.Position); - Assert.Equal(3, viewModel.BucketGroups.ElementAt(2).BucketGroup.Position); - Assert.Equal(4, viewModel.BucketGroups.ElementAt(3).BucketGroup.Position); - Assert.False(viewModel.BucketGroups.First().InModification); - } + Assert.True(result.IsSuccessful); + Assert.Equal(4, viewModel.BucketGroups.Count); + Assert.Equal("New Bucket Group", viewModel.BucketGroups.ElementAt(0).BucketGroup.Name); + Assert.Equal("Bucket Group 1", viewModel.BucketGroups.ElementAt(1).BucketGroup.Name); + Assert.Equal("Bucket Group 3", viewModel.BucketGroups.ElementAt(2).BucketGroup.Name); + Assert.Equal("Bucket Group 2", viewModel.BucketGroups.ElementAt(3).BucketGroup.Name); + Assert.Equal(1, viewModel.BucketGroups.ElementAt(0).BucketGroup.Position); + Assert.Equal(2, viewModel.BucketGroups.ElementAt(1).BucketGroup.Position); + Assert.Equal(3, viewModel.BucketGroups.ElementAt(2).BucketGroup.Position); + Assert.Equal(4, viewModel.BucketGroups.ElementAt(3).BucketGroup.Position); + Assert.False(viewModel.BucketGroups.First().InModification); + } + + [Fact] + public async Task DeleteGroup_CheckGroupDeletionAndPositions() + { + DbConnector.CleanupDatabase(nameof(BucketViewModelIsolatedTest)); - [Fact] - public async Task DeleteGroup_CheckGroupDeletionAndPositions() + using (var dbContext = new DatabaseContext(_dbOptions)) { - DbConnector.CleanupDatabase(nameof(BucketViewModelIsolatedTest)); - - using (var dbContext = new DatabaseContext(_dbOptions)) + dbContext.CreateBucketGroups(new[] { - dbContext.CreateBucketGroups(new[] - { - new BucketGroup() { Name = "Bucket Group 1", Position = 1}, - new BucketGroup() { Name = "Bucket Group 2", Position = 3}, - new BucketGroup() { Name = "Bucket Group 3", Position = 2} - }); - } + new BucketGroup() { Name = "Bucket Group 1", Position = 1}, + new BucketGroup() { Name = "Bucket Group 2", Position = 3}, + new BucketGroup() { Name = "Bucket Group 3", Position = 2} + }); + } - var monthSelectorViewModel = new YearMonthSelectorViewModel(); - var viewModel = new BucketViewModel(_dbOptions, monthSelectorViewModel); - await viewModel.LoadDataAsync(); + var monthSelectorViewModel = new YearMonthSelectorViewModel(); + var viewModel = new BucketViewModel(_dbOptions, monthSelectorViewModel); + await viewModel.LoadDataAsync(); - var groupToDelete = viewModel.BucketGroups.ElementAt(1); - var result = viewModel.DeleteGroup(groupToDelete); - - Assert.True(result.IsSuccessful); - Assert.True(result.ViewModelReloadRequired); + var groupToDelete = viewModel.BucketGroups.ElementAt(1); + var result = viewModel.DeleteGroup(groupToDelete); + + Assert.True(result.IsSuccessful); + Assert.True(result.ViewModelReloadRequired); - await viewModel.LoadDataAsync(); - - Assert.Equal(2, viewModel.BucketGroups.Count); - Assert.Equal("Bucket Group 1", viewModel.BucketGroups.ElementAt(0).BucketGroup.Name); - Assert.Equal("Bucket Group 2", viewModel.BucketGroups.ElementAt(1).BucketGroup.Name); - Assert.Equal(1, viewModel.BucketGroups.ElementAt(0).BucketGroup.Position); - Assert.Equal(2, viewModel.BucketGroups.ElementAt(1).BucketGroup.Position); - - // Reload ViewModel to see if changes has been also reflected onto the database - await viewModel.LoadDataAsync(); - - Assert.Equal(2, viewModel.BucketGroups.Count); - Assert.Equal("Bucket Group 1", viewModel.BucketGroups.ElementAt(0).BucketGroup.Name); - Assert.Equal("Bucket Group 2", viewModel.BucketGroups.ElementAt(1).BucketGroup.Name); - Assert.Equal(1, viewModel.BucketGroups.ElementAt(0).BucketGroup.Position); - Assert.Equal(2, viewModel.BucketGroups.ElementAt(1).BucketGroup.Position); - } + await viewModel.LoadDataAsync(); + + Assert.Equal(2, viewModel.BucketGroups.Count); + Assert.Equal("Bucket Group 1", viewModel.BucketGroups.ElementAt(0).BucketGroup.Name); + Assert.Equal("Bucket Group 2", viewModel.BucketGroups.ElementAt(1).BucketGroup.Name); + Assert.Equal(1, viewModel.BucketGroups.ElementAt(0).BucketGroup.Position); + Assert.Equal(2, viewModel.BucketGroups.ElementAt(1).BucketGroup.Position); + + // Reload ViewModel to see if changes has been also reflected onto the database + await viewModel.LoadDataAsync(); + + Assert.Equal(2, viewModel.BucketGroups.Count); + Assert.Equal("Bucket Group 1", viewModel.BucketGroups.ElementAt(0).BucketGroup.Name); + Assert.Equal("Bucket Group 2", viewModel.BucketGroups.ElementAt(1).BucketGroup.Name); + Assert.Equal(1, viewModel.BucketGroups.ElementAt(0).BucketGroup.Position); + Assert.Equal(2, viewModel.BucketGroups.ElementAt(1).BucketGroup.Position); } -} \ No newline at end of file +} diff --git a/OpenBudgeteer.Core.Test/ViewModelTest/BucketViewModelTest.cs b/OpenBudgeteer.Core.Test/ViewModelTest/BucketViewModelTest.cs index b2715ce..b90951b 100644 --- a/OpenBudgeteer.Core.Test/ViewModelTest/BucketViewModelTest.cs +++ b/OpenBudgeteer.Core.Test/ViewModelTest/BucketViewModelTest.cs @@ -9,918 +9,917 @@ using OpenBudgeteer.Core.ViewModels.ItemViewModels; using Xunit; -namespace OpenBudgeteer.Core.Test.ViewModelTest +namespace OpenBudgeteer.Core.Test.ViewModelTest; + +public class BucketViewModelTest { - public class BucketViewModelTest + private readonly DbContextOptions _dbOptions; + + public BucketViewModelTest() { - private readonly DbContextOptions _dbOptions; + _dbOptions = DbConnector.GetDbContextOptions(nameof(BucketViewModelTest)); + } - public BucketViewModelTest() + public static IEnumerable TestData_LoadDataAsync_CheckBucketGroupAssignedBuckets + { + get { - _dbOptions = DbConnector.GetDbContextOptions(nameof(BucketViewModelTest)); + return new[] + { + new object[] {new List {"Bucket 1"}}, + new object[] {new List {"Bucket 1", "Bucket 2", "Bucket 3"}}, + new object[] {new List()}, + }; } - - public static IEnumerable TestData_LoadDataAsync_CheckBucketGroupAssignedBuckets + } + + [Theory] + [MemberData(nameof(TestData_LoadDataAsync_CheckBucketGroupAssignedBuckets))] + public async Task LoadDataAsync_CheckBucketGroupAssignedBuckets(List bucketNames) + { + using (var dbContext = new DatabaseContext(_dbOptions)) { - get + var testBucketGroup = new BucketGroup() { Name = "Bucket Group", Position = 1 }; + dbContext.CreateBucketGroup(testBucketGroup); + + foreach (var bucketName in bucketNames) { - return new[] + dbContext.CreateBucket(new Bucket() { - new object[] {new List {"Bucket 1"}}, - new object[] {new List {"Bucket 1", "Bucket 2", "Bucket 3"}}, - new object[] {new List()}, - }; + BucketGroupId = testBucketGroup.BucketGroupId, + Name = bucketName, + ColorCode = "Red", + ValidFrom = new DateTime(2010, 1, 1) + }); } - } - - [Theory] - [MemberData(nameof(TestData_LoadDataAsync_CheckBucketGroupAssignedBuckets))] - public async Task LoadDataAsync_CheckBucketGroupAssignedBuckets(List bucketNames) - { - using (var dbContext = new DatabaseContext(_dbOptions)) + + var monthSelectorViewModel = new YearMonthSelectorViewModel(); + var viewModel = new BucketViewModel(_dbOptions, monthSelectorViewModel); + await viewModel.LoadDataAsync(); + + var testObject = viewModel.BucketGroups + .FirstOrDefault(i => i.BucketGroup.BucketGroupId == testBucketGroup.BucketGroupId); + + Assert.NotNull(testObject); + Assert.Equal(bucketNames.Count, testObject.Buckets.Count); + foreach (var bucketName in bucketNames) { - var testBucketGroup = new BucketGroup() { Name = "Bucket Group", Position = 1 }; - dbContext.CreateBucketGroup(testBucketGroup); - - foreach (var bucketName in bucketNames) - { - dbContext.CreateBucket(new Bucket() - { - BucketGroupId = testBucketGroup.BucketGroupId, - Name = bucketName, - ColorCode = "Red", - ValidFrom = new DateTime(2010, 1, 1) - }); - } - - var monthSelectorViewModel = new YearMonthSelectorViewModel(); - var viewModel = new BucketViewModel(_dbOptions, monthSelectorViewModel); - await viewModel.LoadDataAsync(); - - var testObject = viewModel.BucketGroups - .FirstOrDefault(i => i.BucketGroup.BucketGroupId == testBucketGroup.BucketGroupId); - - Assert.NotNull(testObject); - Assert.Equal(bucketNames.Count, testObject.Buckets.Count); - foreach (var bucketName in bucketNames) - { - Assert.Contains(testObject.Buckets, i => i.Bucket.Name == bucketName); - } + Assert.Contains(testObject.Buckets, i => i.Bucket.Name == bucketName); } } + } - public static IEnumerable TestData_LoadDataAsync_CheckBucketSorting + public static IEnumerable TestData_LoadDataAsync_CheckBucketSorting + { + get { - get + return new[] { - return new[] - { - new object[] {new List {"A_Bucket 1", "C_Bucket 2", "B_Bucket 3"}, new List {"A_Bucket 1", "B_Bucket 3", "C_Bucket 2"} } - }; - } + new object[] {new List {"A_Bucket 1", "C_Bucket 2", "B_Bucket 3"}, new List {"A_Bucket 1", "B_Bucket 3", "C_Bucket 2"} } + }; } - - [Theory] - [MemberData(nameof(TestData_LoadDataAsync_CheckBucketSorting))] - public async Task LoadDataAsync_CheckBucketSorting(List bucketNamesUnsorted, List expectedBucketNamesSorted) + } + + [Theory] + [MemberData(nameof(TestData_LoadDataAsync_CheckBucketSorting))] + public async Task LoadDataAsync_CheckBucketSorting(List bucketNamesUnsorted, List expectedBucketNamesSorted) + { + using (var dbContext = new DatabaseContext(_dbOptions)) { - using (var dbContext = new DatabaseContext(_dbOptions)) + var testBucketGroup = new BucketGroup() { Name = "Bucket Group", Position = 1 }; + dbContext.CreateBucketGroup(testBucketGroup); + + foreach (var bucketName in bucketNamesUnsorted) { - var testBucketGroup = new BucketGroup() { Name = "Bucket Group", Position = 1 }; - dbContext.CreateBucketGroup(testBucketGroup); - - foreach (var bucketName in bucketNamesUnsorted) + dbContext.CreateBucket(new Bucket() { - dbContext.CreateBucket(new Bucket() - { - BucketGroupId = testBucketGroup.BucketGroupId, - Name = bucketName, - ColorCode = "Red", - ValidFrom = new DateTime(2010, 1, 1) - }); - } - - var monthSelectorViewModel = new YearMonthSelectorViewModel(); - var viewModel = new BucketViewModel(_dbOptions, monthSelectorViewModel); - await viewModel.LoadDataAsync(); + BucketGroupId = testBucketGroup.BucketGroupId, + Name = bucketName, + ColorCode = "Red", + ValidFrom = new DateTime(2010, 1, 1) + }); + } + + var monthSelectorViewModel = new YearMonthSelectorViewModel(); + var viewModel = new BucketViewModel(_dbOptions, monthSelectorViewModel); + await viewModel.LoadDataAsync(); - var bucketGroup = viewModel.BucketGroups.FirstOrDefault(i => i.BucketGroup.BucketGroupId == testBucketGroup.BucketGroupId); - Assert.NotNull(bucketGroup); - Assert.Equal(bucketNamesUnsorted.Count, bucketGroup.Buckets.Count); + var bucketGroup = viewModel.BucketGroups.FirstOrDefault(i => i.BucketGroup.BucketGroupId == testBucketGroup.BucketGroupId); + Assert.NotNull(bucketGroup); + Assert.Equal(bucketNamesUnsorted.Count, bucketGroup.Buckets.Count); - for (int i = 0; i < bucketGroup.Buckets.Count; i++) - { - Assert.Equal( - expectedBucketNamesSorted.ElementAt(i), - bucketGroup.Buckets.ElementAt(i).Bucket.Name); - } + for (int i = 0; i < bucketGroup.Buckets.Count; i++) + { + Assert.Equal( + expectedBucketNamesSorted.ElementAt(i), + bucketGroup.Buckets.ElementAt(i).Bucket.Name); } } + } - public static IEnumerable TestData_LoadDataAsync_LoadOnlyActiveBuckets + public static IEnumerable TestData_LoadDataAsync_LoadOnlyActiveBuckets + { + get { - get + return new[] { - return new[] - { - // Active in current month - new object[] { new DateTime(2010,1,1), new DateTime(2010,1,1), false, null, true}, - // Active starting next month - new object[] { new DateTime(2010,1,1), new DateTime(2010,2,1), false, null, false}, - // Active starting next year - new object[] { new DateTime(2010,1,1), new DateTime(2011,1,1), false, null, false}, - // Inactive since current month - new object[] { new DateTime(2010,1,1), new DateTime(2009,1,1), true, new DateTime(2010,1,1), false}, - // Inactive since last year - new object[] { new DateTime(2010,1,1), new DateTime(2009,1,1), true, new DateTime(2009,1,1), false}, - // Inactive since last month - new object[] { new DateTime(2010,2,1), new DateTime(2009,1,1), true, new DateTime(2010,1,1), false}, - // Inactive starting next month - new object[] { new DateTime(2010,1,1), new DateTime(2010,1,1), true, new DateTime(2010,2,1), true}, - // Active starting next month but already inactive in the future - new object[] { new DateTime(2010,1,1), new DateTime(2010,2,1), true, new DateTime(2010,3,1), false} - }; - } + // Active in current month + new object[] { new DateTime(2010,1,1), new DateTime(2010,1,1), false, null, true}, + // Active starting next month + new object[] { new DateTime(2010,1,1), new DateTime(2010,2,1), false, null, false}, + // Active starting next year + new object[] { new DateTime(2010,1,1), new DateTime(2011,1,1), false, null, false}, + // Inactive since current month + new object[] { new DateTime(2010,1,1), new DateTime(2009,1,1), true, new DateTime(2010,1,1), false}, + // Inactive since last year + new object[] { new DateTime(2010,1,1), new DateTime(2009,1,1), true, new DateTime(2009,1,1), false}, + // Inactive since last month + new object[] { new DateTime(2010,2,1), new DateTime(2009,1,1), true, new DateTime(2010,1,1), false}, + // Inactive starting next month + new object[] { new DateTime(2010,1,1), new DateTime(2010,1,1), true, new DateTime(2010,2,1), true}, + // Active starting next month but already inactive in the future + new object[] { new DateTime(2010,1,1), new DateTime(2010,2,1), true, new DateTime(2010,3,1), false} + }; } - - [Theory] - [MemberData(nameof(TestData_LoadDataAsync_LoadOnlyActiveBuckets))] - public async Task LoadDataAsync_LoadOnlyActiveBuckets( - DateTime testMonth, - DateTime bucketActiveSince, - bool bucketIsInactive, - DateTime bucketIsInActiveFrom, - bool expectedBucketAvailable - ) + } + + [Theory] + [MemberData(nameof(TestData_LoadDataAsync_LoadOnlyActiveBuckets))] + public async Task LoadDataAsync_LoadOnlyActiveBuckets( + DateTime testMonth, + DateTime bucketActiveSince, + bool bucketIsInactive, + DateTime bucketIsInActiveFrom, + bool expectedBucketAvailable + ) + { + using (var dbContext = new DatabaseContext(_dbOptions)) + { + var testAccount = new Account() {IsActive = 1, Name = "Account"}; + var testBucketGroup = new BucketGroup() {Name = "Bucket Group", Position = 1}; + + dbContext.CreateAccount(testAccount); + dbContext.CreateBucketGroup(testBucketGroup); + + var testBucket = new Bucket() + { + BucketGroupId = testBucketGroup.BucketGroupId, + Name = "Bucket", + ValidFrom = bucketActiveSince, + IsInactive = bucketIsInactive, + IsInactiveFrom = bucketIsInActiveFrom, + }; + + dbContext.CreateBucket(testBucket); + dbContext.CreateBucketVersion(new BucketVersion() + { + BucketId = testBucket.BucketId, + Version = 1, + BucketType = 1, + ValidFrom = bucketActiveSince + }); + + var monthSelectorViewModel = new YearMonthSelectorViewModel() + { + SelectedYear = testMonth.Year, + SelectedMonth = testMonth.Month + }; + var viewModel = new BucketViewModel(_dbOptions, monthSelectorViewModel); + await viewModel.LoadDataAsync(); + + var bucketGroup = viewModel.BucketGroups.FirstOrDefault(i => i.BucketGroup.BucketGroupId == testBucketGroup.BucketGroupId); + Assert.NotNull(bucketGroup); + + Assert.Equal(expectedBucketAvailable, bucketGroup.Buckets.Any()); + } + } + + [Fact] + public async Task LoadDataAsync_CheckValidFromHandling() + { + try { using (var dbContext = new DatabaseContext(_dbOptions)) { - var testAccount = new Account() {IsActive = 1, Name = "Account"}; - var testBucketGroup = new BucketGroup() {Name = "Bucket Group", Position = 1}; + var testAccount = new Account() { IsActive = 1, Name = "Account" }; + var testBucketGroup = new BucketGroup() { Name = "Bucket Group", Position = 1 }; dbContext.CreateAccount(testAccount); dbContext.CreateBucketGroup(testBucketGroup); - var testBucket = new Bucket() + var testBucket1 = new Bucket() + { + BucketGroupId = testBucketGroup.BucketGroupId, + Name = "Bucket Active Current Month", + ValidFrom = new DateTime(2010, 1, 1) + }; + var testBucket2 = new Bucket() + { + BucketGroupId = testBucketGroup.BucketGroupId, + Name = "Bucket Active Past", + ValidFrom = new DateTime(2009, 1, 1) + }; + var testBucket3 = new Bucket() { - BucketGroupId = testBucketGroup.BucketGroupId, - Name = "Bucket", - ValidFrom = bucketActiveSince, - IsInactive = bucketIsInactive, - IsInactiveFrom = bucketIsInActiveFrom, + BucketGroupId = testBucketGroup.BucketGroupId, + Name = "Bucket Active Future", + ValidFrom = new DateTime(2010, 2, 1) }; - dbContext.CreateBucket(testBucket); - dbContext.CreateBucketVersion(new BucketVersion() + dbContext.CreateBuckets(new[] + { + testBucket1, testBucket2, testBucket3 + }); + dbContext.CreateBucketVersions(new[] { - BucketId = testBucket.BucketId, - Version = 1, - BucketType = 1, - ValidFrom = bucketActiveSince + new BucketVersion() { BucketId = testBucket1.BucketId, Version = 1, BucketType = 1, ValidFrom = testBucket1.ValidFrom }, + new BucketVersion() { BucketId = testBucket2.BucketId, Version = 1, BucketType = 1, ValidFrom = testBucket2.ValidFrom }, + new BucketVersion() { BucketId = testBucket3.BucketId, Version = 1, BucketType = 1, ValidFrom = testBucket3.ValidFrom }, }); - + + var monthSelectorViewModel = new YearMonthSelectorViewModel() { - SelectedYear = testMonth.Year, - SelectedMonth = testMonth.Month + SelectedYear = 2010, + SelectedMonth = 1 }; var viewModel = new BucketViewModel(_dbOptions, monthSelectorViewModel); await viewModel.LoadDataAsync(); - + var bucketGroup = viewModel.BucketGroups.FirstOrDefault(i => i.BucketGroup.BucketGroupId == testBucketGroup.BucketGroupId); Assert.NotNull(bucketGroup); - Assert.Equal(expectedBucketAvailable, bucketGroup.Buckets.Any()); + Assert.Equal(2, bucketGroup.Buckets.Count); + Assert.Contains(bucketGroup.Buckets, i => i.Bucket.BucketId == testBucket1.BucketId); + Assert.Contains(bucketGroup.Buckets, i => i.Bucket.BucketId == testBucket2.BucketId); + Assert.DoesNotContain(bucketGroup.Buckets, i => i.Bucket.BucketId == testBucket3.BucketId); } } + finally + { + DbConnector.CleanupDatabase(nameof(BucketViewModelTest)); + } + } - [Fact] - public async Task LoadDataAsync_CheckValidFromHandling() + [Fact] + public async Task LoadDataAsync_CheckCalculatedValues() + { + try { - try + using (var dbContext = new DatabaseContext(_dbOptions)) { - using (var dbContext = new DatabaseContext(_dbOptions)) - { - var testAccount = new Account() { IsActive = 1, Name = "Account" }; - var testBucketGroup = new BucketGroup() { Name = "Bucket Group", Position = 1 }; - - dbContext.CreateAccount(testAccount); - dbContext.CreateBucketGroup(testBucketGroup); - - var testBucket1 = new Bucket() - { - BucketGroupId = testBucketGroup.BucketGroupId, - Name = "Bucket Active Current Month", - ValidFrom = new DateTime(2010, 1, 1) - }; - var testBucket2 = new Bucket() - { - BucketGroupId = testBucketGroup.BucketGroupId, - Name = "Bucket Active Past", - ValidFrom = new DateTime(2009, 1, 1) - }; - var testBucket3 = new Bucket() - { - BucketGroupId = testBucketGroup.BucketGroupId, - Name = "Bucket Active Future", - ValidFrom = new DateTime(2010, 2, 1) - }; + var testAccount = new Account() { IsActive = 1, Name = "Account" }; + var testBucketGroup = new BucketGroup() { Name = "Bucket Group", Position = 1 }; - dbContext.CreateBuckets(new[] - { - testBucket1, testBucket2, testBucket3 - }); - dbContext.CreateBucketVersions(new[] - { - new BucketVersion() { BucketId = testBucket1.BucketId, Version = 1, BucketType = 1, ValidFrom = testBucket1.ValidFrom }, - new BucketVersion() { BucketId = testBucket2.BucketId, Version = 1, BucketType = 1, ValidFrom = testBucket2.ValidFrom }, - new BucketVersion() { BucketId = testBucket3.BucketId, Version = 1, BucketType = 1, ValidFrom = testBucket3.ValidFrom }, - }); + dbContext.CreateAccount(testAccount); + dbContext.CreateBucketGroup(testBucketGroup); + var testBucket1 = new Bucket() + { + BucketGroupId = testBucketGroup.BucketGroupId, + Name = "Bucket 1", + ValidFrom = new DateTime(2010, 1, 1) + }; + var testBucket2 = new Bucket() + { + BucketGroupId = testBucketGroup.BucketGroupId, + Name = "Bucket 2", + ValidFrom = new DateTime(2010, 1, 1) + }; - var monthSelectorViewModel = new YearMonthSelectorViewModel() - { - SelectedYear = 2010, - SelectedMonth = 1 - }; - var viewModel = new BucketViewModel(_dbOptions, monthSelectorViewModel); - await viewModel.LoadDataAsync(); - - var bucketGroup = viewModel.BucketGroups.FirstOrDefault(i => i.BucketGroup.BucketGroupId == testBucketGroup.BucketGroupId); - Assert.NotNull(bucketGroup); - - Assert.Equal(2, bucketGroup.Buckets.Count); - Assert.Contains(bucketGroup.Buckets, i => i.Bucket.BucketId == testBucket1.BucketId); - Assert.Contains(bucketGroup.Buckets, i => i.Bucket.BucketId == testBucket2.BucketId); - Assert.DoesNotContain(bucketGroup.Buckets, i => i.Bucket.BucketId == testBucket3.BucketId); - } - } - finally - { - DbConnector.CleanupDatabase(nameof(BucketViewModelTest)); - } - } + dbContext.CreateBuckets(new[] + { + testBucket1, testBucket2 + }); + dbContext.CreateBucketVersions(new[] + { + new BucketVersion() { BucketId = testBucket1.BucketId, Version = 1, BucketType = 1, ValidFrom = testBucket1.ValidFrom }, + new BucketVersion() { BucketId = testBucket2.BucketId, Version = 1, BucketType = 1, ValidFrom = testBucket2.ValidFrom }, + }); - [Fact] - public async Task LoadDataAsync_CheckCalculatedValues() - { - try - { - using (var dbContext = new DatabaseContext(_dbOptions)) + var testTransactions = new[] { - var testAccount = new Account() { IsActive = 1, Name = "Account" }; - var testBucketGroup = new BucketGroup() { Name = "Bucket Group", Position = 1 }; + new BankTransaction() { AccountId = testAccount.AccountId, TransactionDate = new DateTime(2010,1,1), Amount = 1 }, + new BankTransaction() { AccountId = testAccount.AccountId, TransactionDate = new DateTime(2010,1,1), Amount = -10 }, + new BankTransaction() { AccountId = testAccount.AccountId, TransactionDate = new DateTime(2010,1,1), Amount = 100 }, + new BankTransaction() { AccountId = testAccount.AccountId, TransactionDate = new DateTime(2010,1,1), Amount = -1000 }, + new BankTransaction() { AccountId = testAccount.AccountId, TransactionDate = new DateTime(2010,1,1), Amount = 10000 }, + new BankTransaction() { AccountId = testAccount.AccountId, TransactionDate = new DateTime(2009,1,1), Amount = 100000 }, + new BankTransaction() { AccountId = testAccount.AccountId, TransactionDate = new DateTime(2010,2,1), Amount = 1000000 }, + }; + dbContext.CreateBankTransactions(testTransactions); - dbContext.CreateAccount(testAccount); - dbContext.CreateBucketGroup(testBucketGroup); + dbContext.CreateBudgetedTransactions(new[] + { + new BudgetedTransaction() { TransactionId = testTransactions[0].TransactionId, BucketId = testBucket1.BucketId, Amount = 1 }, + new BudgetedTransaction() { TransactionId = testTransactions[1].TransactionId, BucketId = testBucket1.BucketId, Amount = -5 }, + new BudgetedTransaction() { TransactionId = testTransactions[1].TransactionId, BucketId = testBucket2.BucketId, Amount = -5 }, + new BudgetedTransaction() { TransactionId = testTransactions[2].TransactionId, BucketId = testBucket1.BucketId, Amount = 100 }, + new BudgetedTransaction() { TransactionId = testTransactions[3].TransactionId, BucketId = testBucket2.BucketId, Amount = -1000 }, + new BudgetedTransaction() { TransactionId = testTransactions[4].TransactionId, BucketId = testBucket2.BucketId, Amount = 10000 }, + new BudgetedTransaction() { TransactionId = testTransactions[5].TransactionId, BucketId = testBucket2.BucketId, Amount = 100000 }, + new BudgetedTransaction() { TransactionId = testTransactions[6].TransactionId, BucketId = testBucket2.BucketId, Amount = 1000000 }, + }); - var testBucket1 = new Bucket() - { - BucketGroupId = testBucketGroup.BucketGroupId, - Name = "Bucket 1", - ValidFrom = new DateTime(2010, 1, 1) - }; - var testBucket2 = new Bucket() - { - BucketGroupId = testBucketGroup.BucketGroupId, - Name = "Bucket 2", - ValidFrom = new DateTime(2010, 1, 1) - }; + var monthSelectorViewModel = new YearMonthSelectorViewModel() + { + SelectedYear = 2010, + SelectedMonth = 1 + }; + var viewModel = new BucketViewModel(_dbOptions, monthSelectorViewModel); + await viewModel.LoadDataAsync(); - dbContext.CreateBuckets(new[] - { - testBucket1, testBucket2 - }); - dbContext.CreateBucketVersions(new[] - { - new BucketVersion() { BucketId = testBucket1.BucketId, Version = 1, BucketType = 1, ValidFrom = testBucket1.ValidFrom }, - new BucketVersion() { BucketId = testBucket2.BucketId, Version = 1, BucketType = 1, ValidFrom = testBucket2.ValidFrom }, - }); + var bucketGroup = viewModel.BucketGroups.FirstOrDefault(i => i.BucketGroup.BucketGroupId == testBucketGroup.BucketGroupId); + Assert.NotNull(bucketGroup); - var testTransactions = new[] - { - new BankTransaction() { AccountId = testAccount.AccountId, TransactionDate = new DateTime(2010,1,1), Amount = 1 }, - new BankTransaction() { AccountId = testAccount.AccountId, TransactionDate = new DateTime(2010,1,1), Amount = -10 }, - new BankTransaction() { AccountId = testAccount.AccountId, TransactionDate = new DateTime(2010,1,1), Amount = 100 }, - new BankTransaction() { AccountId = testAccount.AccountId, TransactionDate = new DateTime(2010,1,1), Amount = -1000 }, - new BankTransaction() { AccountId = testAccount.AccountId, TransactionDate = new DateTime(2010,1,1), Amount = 10000 }, - new BankTransaction() { AccountId = testAccount.AccountId, TransactionDate = new DateTime(2009,1,1), Amount = 100000 }, - new BankTransaction() { AccountId = testAccount.AccountId, TransactionDate = new DateTime(2010,2,1), Amount = 1000000 }, - }; - dbContext.CreateBankTransactions(testTransactions); - - dbContext.CreateBudgetedTransactions(new[] - { - new BudgetedTransaction() { TransactionId = testTransactions[0].TransactionId, BucketId = testBucket1.BucketId, Amount = 1 }, - new BudgetedTransaction() { TransactionId = testTransactions[1].TransactionId, BucketId = testBucket1.BucketId, Amount = -5 }, - new BudgetedTransaction() { TransactionId = testTransactions[1].TransactionId, BucketId = testBucket2.BucketId, Amount = -5 }, - new BudgetedTransaction() { TransactionId = testTransactions[2].TransactionId, BucketId = testBucket1.BucketId, Amount = 100 }, - new BudgetedTransaction() { TransactionId = testTransactions[3].TransactionId, BucketId = testBucket2.BucketId, Amount = -1000 }, - new BudgetedTransaction() { TransactionId = testTransactions[4].TransactionId, BucketId = testBucket2.BucketId, Amount = 10000 }, - new BudgetedTransaction() { TransactionId = testTransactions[5].TransactionId, BucketId = testBucket2.BucketId, Amount = 100000 }, - new BudgetedTransaction() { TransactionId = testTransactions[6].TransactionId, BucketId = testBucket2.BucketId, Amount = 1000000 }, - }); - - var monthSelectorViewModel = new YearMonthSelectorViewModel() - { - SelectedYear = 2010, - SelectedMonth = 1 - }; - var viewModel = new BucketViewModel(_dbOptions, monthSelectorViewModel); - await viewModel.LoadDataAsync(); - - var bucketGroup = viewModel.BucketGroups.FirstOrDefault(i => i.BucketGroup.BucketGroupId == testBucketGroup.BucketGroupId); - Assert.NotNull(bucketGroup); - - // This test includes: - // - Bucket Split - var testObject = bucketGroup.Buckets.FirstOrDefault(i => i.Bucket.BucketId == testBucket1.BucketId); - Assert.NotNull(testObject); - Assert.Equal(-5, testObject.Activity); - Assert.Equal(96, testObject.Balance); - Assert.Equal(101, testObject.In); - - // This test includes: - // - Bucket Split - // - Include Transactions in previous months for Balance - // - Exclude Transactions in the future - testObject = bucketGroup.Buckets.FirstOrDefault(i => i.Bucket.BucketId == testBucket2.BucketId); - Assert.NotNull(testObject); - Assert.Equal(-1005, testObject.Activity); - Assert.Equal(108995, testObject.Balance); - Assert.Equal(10000, testObject.In); - } - } - finally - { - DbConnector.CleanupDatabase(nameof(BucketViewModelTest)); + // This test includes: + // - Bucket Split + var testObject = bucketGroup.Buckets.FirstOrDefault(i => i.Bucket.BucketId == testBucket1.BucketId); + Assert.NotNull(testObject); + Assert.Equal(-5, testObject.Activity); + Assert.Equal(96, testObject.Balance); + Assert.Equal(101, testObject.In); + + // This test includes: + // - Bucket Split + // - Include Transactions in previous months for Balance + // - Exclude Transactions in the future + testObject = bucketGroup.Buckets.FirstOrDefault(i => i.Bucket.BucketId == testBucket2.BucketId); + Assert.NotNull(testObject); + Assert.Equal(-1005, testObject.Activity); + Assert.Equal(108995, testObject.Balance); + Assert.Equal(10000, testObject.In); } } + finally + { + DbConnector.CleanupDatabase(nameof(BucketViewModelTest)); + } + } - public static IEnumerable TestData_CheckWantAndDetailCalculation_MonthlyExpenses + public static IEnumerable TestData_CheckWantAndDetailCalculation_MonthlyExpenses + { + get { - get + return new[] { - return new[] + new object[] + { + new Bucket { Name = "Bucket with pending Want", ValidFrom = new DateTime(2010,1,1) }, + new BucketVersion { Version = 1, BucketType = 2, BucketTypeYParam = 10 }, + new List(), + new List(), + 10, 0, 0 + }, + new object[] { - new object[] + new Bucket { Name = "Bucket with fulfilled Want", ValidFrom = new DateTime(2010,1,1) }, + new BucketVersion { Version = 1, BucketType = 2, BucketTypeYParam = 10 }, + new List(), + new List() { - new Bucket { Name = "Bucket with pending Want", ValidFrom = new DateTime(2010,1,1) }, - new BucketVersion { Version = 1, BucketType = 2, BucketTypeYParam = 10 }, - new List(), - new List(), - 10, 0, 0 + new BucketMovement { MovementDate = new DateTime(2010, 1, 1), Amount = 10 } }, - new object[] + 0, 10 ,0 + }, + new object[] + { + new Bucket { Name = "Bucket pending Want including expense", ValidFrom = new DateTime(2010,1,1) }, + new BucketVersion { Version = 1, BucketType = 2, BucketTypeYParam = 10 }, + new List { - new Bucket { Name = "Bucket with fulfilled Want", ValidFrom = new DateTime(2010,1,1) }, - new BucketVersion { Version = 1, BucketType = 2, BucketTypeYParam = 10 }, - new List(), - new List() - { - new BucketMovement { MovementDate = new DateTime(2010, 1, 1), Amount = 10 } - }, - 0, 10 ,0 + new BankTransaction { TransactionDate = new DateTime(2010,1,1), Amount = -10 } }, - new object[] + new List(), + 10, 0, -10 + }, + new object[] + { + new Bucket { Name = "Bucket fulfilled Want including expense", ValidFrom = new DateTime(2010,1,1) }, + new BucketVersion { Version = 1, BucketType = 2, BucketTypeYParam = 10 }, + new List { - new Bucket { Name = "Bucket pending Want including expense", ValidFrom = new DateTime(2010,1,1) }, - new BucketVersion { Version = 1, BucketType = 2, BucketTypeYParam = 10 }, - new List - { - new BankTransaction { TransactionDate = new DateTime(2010,1,1), Amount = -10 } - }, - new List(), - 10, 0, -10 + new BankTransaction { TransactionDate = new DateTime(2010,1,1), Amount = -10 } }, - new object[] + new List { - new Bucket { Name = "Bucket fulfilled Want including expense", ValidFrom = new DateTime(2010,1,1) }, - new BucketVersion { Version = 1, BucketType = 2, BucketTypeYParam = 10 }, - new List - { - new BankTransaction { TransactionDate = new DateTime(2010,1,1), Amount = -10 } - }, - new List - { - new BucketMovement { MovementDate = new DateTime(2010, 1, 1), Amount = 10 } - }, - 0, 10, -10 + new BucketMovement { MovementDate = new DateTime(2010, 1, 1), Amount = 10 } }, - new object[] + 0, 10, -10 + }, + new object[] + { + new Bucket { Name = "Bucket with partial fulfilled Want", ValidFrom = new DateTime(2010,1,1) }, + new BucketVersion { Version = 1, BucketType = 2, BucketTypeYParam = 10 }, + new List(), + new List { - new Bucket { Name = "Bucket with partial fulfilled Want", ValidFrom = new DateTime(2010,1,1) }, - new BucketVersion { Version = 1, BucketType = 2, BucketTypeYParam = 10 }, - new List(), - new List - { - new BucketMovement { MovementDate = new DateTime(2010, 1, 1), Amount = 5 } - }, - 5, 5, 0 + new BucketMovement { MovementDate = new DateTime(2010, 1, 1), Amount = 5 } }, - new object[] + 5, 5, 0 + }, + new object[] + { + new Bucket { Name = "Bucket with over fulfilled Want", ValidFrom = new DateTime(2010,1,1) }, + new BucketVersion { Version = 1, BucketType = 2, BucketTypeYParam = 10 }, + new List(), + new List { - new Bucket { Name = "Bucket with over fulfilled Want", ValidFrom = new DateTime(2010,1,1) }, - new BucketVersion { Version = 1, BucketType = 2, BucketTypeYParam = 10 }, - new List(), - new List - { - new BucketMovement { MovementDate = new DateTime(2010, 1, 1), Amount = 15 } - }, - 0, 15 ,0 - } - }; - } + new BucketMovement { MovementDate = new DateTime(2010, 1, 1), Amount = 15 } + }, + 0, 15 ,0 + } + }; } + } - [Theory] - [MemberData(nameof(TestData_CheckWantAndDetailCalculation_MonthlyExpenses))] - public async Task LoadDataAsync_CheckWantAndDetailCalculation_MonthlyExpenses( - Bucket testBucket, - BucketVersion testBucketVersion, - List testTransactions, - List testBucketMovements, - decimal expectedWant, - decimal expectedIn, - decimal expectedActivity - ) + [Theory] + [MemberData(nameof(TestData_CheckWantAndDetailCalculation_MonthlyExpenses))] + public async Task LoadDataAsync_CheckWantAndDetailCalculation_MonthlyExpenses( + Bucket testBucket, + BucketVersion testBucketVersion, + List testTransactions, + List testBucketMovements, + decimal expectedWant, + decimal expectedIn, + decimal expectedActivity + ) + { + try { - try - { - var testObject = await ExecuteBucketCreationAndTransactionMovementsAsync( - testBucket, testBucketVersion, testTransactions, testBucketMovements, new DateTime(2010,1,1)); + var testObject = await ExecuteBucketCreationAndTransactionMovementsAsync( + testBucket, testBucketVersion, testTransactions, testBucketMovements, new DateTime(2010,1,1)); - Assert.Equal(expectedWant, testObject.Want); - Assert.Equal(expectedIn, testObject.In); - Assert.Equal(expectedActivity, testObject.Activity); + Assert.Equal(expectedWant, testObject.Want); + Assert.Equal(expectedIn, testObject.In); + Assert.Equal(expectedActivity, testObject.Activity); - } - finally - { - DbConnector.CleanupDatabase(nameof(BucketViewModelTest)); - } } + finally + { + DbConnector.CleanupDatabase(nameof(BucketViewModelTest)); + } + } - public static IEnumerable TestData_CheckWantAndDetailCalculation_ExpenseEveryXMonths + public static IEnumerable TestData_CheckWantAndDetailCalculation_ExpenseEveryXMonths + { + get { - get + return new[] { - return new[] + new object[] + { + new Bucket { Name = "120 every 12 months, with Want", ValidFrom = new DateTime(2010,1,1) }, + new BucketVersion { Version = 1, BucketType = 3, BucketTypeXParam = 12, BucketTypeYParam = 120, BucketTypeZParam = new DateTime(2010,12,1) }, + new List(), + new List(), + 10, 0, 0, 0, "120 until 2010-12", 0 + }, + new object[] { - new object[] + new Bucket { Name = "120 every 12 months, without Want", ValidFrom = new DateTime(2010,1,1) }, + new BucketVersion { Version = 1, BucketType = 3, BucketTypeXParam = 12, BucketTypeYParam = 120, BucketTypeZParam = new DateTime(2010,12,1) }, + new List(), + new List { - new Bucket { Name = "120 every 12 months, with Want", ValidFrom = new DateTime(2010,1,1) }, - new BucketVersion { Version = 1, BucketType = 3, BucketTypeXParam = 12, BucketTypeYParam = 120, BucketTypeZParam = new DateTime(2010,12,1) }, - new List(), - new List(), - 10, 0, 0, 0, "120 until 2010-12", 0 + new BucketMovement { MovementDate = new DateTime(2010,1,1), Amount = 10 } }, - new object[] - { - new Bucket { Name = "120 every 12 months, without Want", ValidFrom = new DateTime(2010,1,1) }, - new BucketVersion { Version = 1, BucketType = 3, BucketTypeXParam = 12, BucketTypeYParam = 120, BucketTypeZParam = new DateTime(2010,12,1) }, - new List(), - new List - { - new BucketMovement { MovementDate = new DateTime(2010,1,1), Amount = 10 } - }, - 0, 10, 0, 10, "120 until 2010-12", 8 + 0, 10, 0, 10, "120 until 2010-12", 8 + }, + new object[] + { + new Bucket { Name = "120 every 12 months, last 6 months, with Want", ValidFrom = new DateTime(2009,7,1) }, + new BucketVersion { Version = 1, BucketType = 3, BucketTypeXParam = 12, BucketTypeYParam = 120, BucketTypeZParam = new DateTime(2010,6,1) }, + new List(), + new List + { + new BucketMovement { MovementDate = new DateTime(2009,7,1), Amount = 10 }, + new BucketMovement { MovementDate = new DateTime(2009,8,1), Amount = 10 }, + new BucketMovement { MovementDate = new DateTime(2009,9,1), Amount = 10 }, + new BucketMovement { MovementDate = new DateTime(2009,10,1), Amount = 10 }, + new BucketMovement { MovementDate = new DateTime(2009,11,1), Amount = 10 }, + new BucketMovement { MovementDate = new DateTime(2009,12,1), Amount = 10 } }, - new object[] - { - new Bucket { Name = "120 every 12 months, last 6 months, with Want", ValidFrom = new DateTime(2009,7,1) }, - new BucketVersion { Version = 1, BucketType = 3, BucketTypeXParam = 12, BucketTypeYParam = 120, BucketTypeZParam = new DateTime(2010,6,1) }, - new List(), - new List - { - new BucketMovement { MovementDate = new DateTime(2009,7,1), Amount = 10 }, - new BucketMovement { MovementDate = new DateTime(2009,8,1), Amount = 10 }, - new BucketMovement { MovementDate = new DateTime(2009,9,1), Amount = 10 }, - new BucketMovement { MovementDate = new DateTime(2009,10,1), Amount = 10 }, - new BucketMovement { MovementDate = new DateTime(2009,11,1), Amount = 10 }, - new BucketMovement { MovementDate = new DateTime(2009,12,1), Amount = 10 } - }, - 10, 0, 0, 60, "120 until 2010-06", 50 + 10, 0, 0, 60, "120 until 2010-06", 50 + }, + new object[] + { + new Bucket { Name = "120 every 12 months, last 6 months, without Want", ValidFrom = new DateTime(2009,7,1) }, + new BucketVersion { Version = 1, BucketType = 3, BucketTypeXParam = 12, BucketTypeYParam = 120, BucketTypeZParam = new DateTime(2010,6,1) }, + new List(), + new List + { + new BucketMovement { MovementDate = new DateTime(2009,7,1), Amount = 10 }, + new BucketMovement { MovementDate = new DateTime(2009,8,1), Amount = 10 }, + new BucketMovement { MovementDate = new DateTime(2009,9,1), Amount = 10 }, + new BucketMovement { MovementDate = new DateTime(2009,10,1), Amount = 10 }, + new BucketMovement { MovementDate = new DateTime(2009,11,1), Amount = 10 }, + new BucketMovement { MovementDate = new DateTime(2009,12,1), Amount = 10 }, + new BucketMovement { MovementDate = new DateTime(2010,1,1), Amount = 10 } }, - new object[] - { - new Bucket { Name = "120 every 12 months, last 6 months, without Want", ValidFrom = new DateTime(2009,7,1) }, - new BucketVersion { Version = 1, BucketType = 3, BucketTypeXParam = 12, BucketTypeYParam = 120, BucketTypeZParam = new DateTime(2010,6,1) }, - new List(), - new List - { - new BucketMovement { MovementDate = new DateTime(2009,7,1), Amount = 10 }, - new BucketMovement { MovementDate = new DateTime(2009,8,1), Amount = 10 }, - new BucketMovement { MovementDate = new DateTime(2009,9,1), Amount = 10 }, - new BucketMovement { MovementDate = new DateTime(2009,10,1), Amount = 10 }, - new BucketMovement { MovementDate = new DateTime(2009,11,1), Amount = 10 }, - new BucketMovement { MovementDate = new DateTime(2009,12,1), Amount = 10 }, - new BucketMovement { MovementDate = new DateTime(2010,1,1), Amount = 10 } - }, - 0, 10, 0, 70, "120 until 2010-06", 58 + 0, 10, 0, 70, "120 until 2010-06", 58 + }, + new object[] + { + new Bucket { Name = "120 every 12 months, last 6 months, fulfilled target", ValidFrom = new DateTime(2009,7,1) }, + new BucketVersion { Version = 1, BucketType = 3, BucketTypeXParam = 12, BucketTypeYParam = 120, BucketTypeZParam = new DateTime(2010,6,1) }, + new List(), + new List + { + new BucketMovement { MovementDate = new DateTime(2009,7,1), Amount = 20 }, + new BucketMovement { MovementDate = new DateTime(2009,8,1), Amount = 20 }, + new BucketMovement { MovementDate = new DateTime(2009,9,1), Amount = 20 }, + new BucketMovement { MovementDate = new DateTime(2009,10,1), Amount = 20 }, + new BucketMovement { MovementDate = new DateTime(2009,11,1), Amount = 20 }, + new BucketMovement { MovementDate = new DateTime(2009,12,1), Amount = 20 } }, - new object[] - { - new Bucket { Name = "120 every 12 months, last 6 months, fulfilled target", ValidFrom = new DateTime(2009,7,1) }, - new BucketVersion { Version = 1, BucketType = 3, BucketTypeXParam = 12, BucketTypeYParam = 120, BucketTypeZParam = new DateTime(2010,6,1) }, - new List(), - new List - { - new BucketMovement { MovementDate = new DateTime(2009,7,1), Amount = 20 }, - new BucketMovement { MovementDate = new DateTime(2009,8,1), Amount = 20 }, - new BucketMovement { MovementDate = new DateTime(2009,9,1), Amount = 20 }, - new BucketMovement { MovementDate = new DateTime(2009,10,1), Amount = 20 }, - new BucketMovement { MovementDate = new DateTime(2009,11,1), Amount = 20 }, - new BucketMovement { MovementDate = new DateTime(2009,12,1), Amount = 20 } - }, - 0, 0, 0, 120, "120 until 2010-06", 100 + 0, 0, 0, 120, "120 until 2010-06", 100 + }, + new object[] + { + new Bucket { Name = "120 every 12 months, last 6 months, over-fulfilled target", ValidFrom = new DateTime(2009,7,1) }, + new BucketVersion { Version = 1, BucketType = 3, BucketTypeXParam = 12, BucketTypeYParam = 120, BucketTypeZParam = new DateTime(2010,6,1) }, + new List(), + new List + { + new BucketMovement { MovementDate = new DateTime(2009,7,1), Amount = 20 }, + new BucketMovement { MovementDate = new DateTime(2009,8,1), Amount = 20 }, + new BucketMovement { MovementDate = new DateTime(2009,9,1), Amount = 20 }, + new BucketMovement { MovementDate = new DateTime(2009,10,1), Amount = 20 }, + new BucketMovement { MovementDate = new DateTime(2009,11,1), Amount = 20 }, + new BucketMovement { MovementDate = new DateTime(2009,12,1), Amount = 30 } }, - new object[] - { - new Bucket { Name = "120 every 12 months, last 6 months, over-fulfilled target", ValidFrom = new DateTime(2009,7,1) }, - new BucketVersion { Version = 1, BucketType = 3, BucketTypeXParam = 12, BucketTypeYParam = 120, BucketTypeZParam = new DateTime(2010,6,1) }, - new List(), - new List - { - new BucketMovement { MovementDate = new DateTime(2009,7,1), Amount = 20 }, - new BucketMovement { MovementDate = new DateTime(2009,8,1), Amount = 20 }, - new BucketMovement { MovementDate = new DateTime(2009,9,1), Amount = 20 }, - new BucketMovement { MovementDate = new DateTime(2009,10,1), Amount = 20 }, - new BucketMovement { MovementDate = new DateTime(2009,11,1), Amount = 20 }, - new BucketMovement { MovementDate = new DateTime(2009,12,1), Amount = 30 } - }, - 0, 0, 0, 130, "120 until 2010-06", 100 + 0, 0, 0, 130, "120 until 2010-06", 100 + }, + new object[] + { + new Bucket { Name = "120 every 12 months, last 6 months, no input", ValidFrom = new DateTime(2009,7,1) }, + new BucketVersion { Version = 1, BucketType = 3, BucketTypeXParam = 12, BucketTypeYParam = 120, BucketTypeZParam = new DateTime(2010,6,1) }, + new List(), + new List(), + 20, 0, 0, 0, "120 until 2010-06", 0 + }, + new object[] + { + new Bucket { Name = "120 every 12 months, last 6 months, input not in sync", ValidFrom = new DateTime(2009,7,1) }, + new BucketVersion { Version = 1, BucketType = 3, BucketTypeXParam = 12, BucketTypeYParam = 120, BucketTypeZParam = new DateTime(2010,6,1) }, + new List(), + new List + { + new BucketMovement { MovementDate = new DateTime(2009,7,1), Amount = 10 }, + new BucketMovement { MovementDate = new DateTime(2009,8,1), Amount = 10 }, + new BucketMovement { MovementDate = new DateTime(2009,11,1), Amount = 10 } }, - new object[] + 15, 0, 0, 30, "120 until 2010-06", 25 + }, + new object[] + { + new Bucket { Name = "100 every 3 months, with Want", ValidFrom = new DateTime(2010,1,1) }, + new BucketVersion { Version = 1, BucketType = 3, BucketTypeXParam = 3, BucketTypeYParam = 100, BucketTypeZParam = new DateTime(2010,3,1) }, + new List(), + new List(), + 33.33m, 0, 0, 0, "100 until 2010-03", 0 + }, + new object[] + { + new Bucket { Name = "100 every 3 months, last month, with Want", ValidFrom = new DateTime(2009,11,1) }, + new BucketVersion { Version = 1, BucketType = 3, BucketTypeXParam = 3, BucketTypeYParam = 100, BucketTypeZParam = new DateTime(2010,1,1) }, + new List(), + new List { - new Bucket { Name = "120 every 12 months, last 6 months, no input", ValidFrom = new DateTime(2009,7,1) }, - new BucketVersion { Version = 1, BucketType = 3, BucketTypeXParam = 12, BucketTypeYParam = 120, BucketTypeZParam = new DateTime(2010,6,1) }, - new List(), - new List(), - 20, 0, 0, 0, "120 until 2010-06", 0 + new BucketMovement { MovementDate = new DateTime(2009,11,1), Amount = 33.33m }, + new BucketMovement { MovementDate = new DateTime(2009,12,1), Amount = 33.33m } }, - new object[] + 33.34m, 0, 0, 66.66m, "100 until 2010-01", 67 + }, + new object[] + { + new Bucket { Name = "100 every 3 months, last month, input not in sync", ValidFrom = new DateTime(2009,11,1) }, + new BucketVersion { Version = 1, BucketType = 3, BucketTypeXParam = 3, BucketTypeYParam = 100, BucketTypeZParam = new DateTime(2010,1,1) }, + new List(), + new List { - new Bucket { Name = "120 every 12 months, last 6 months, input not in sync", ValidFrom = new DateTime(2009,7,1) }, - new BucketVersion { Version = 1, BucketType = 3, BucketTypeXParam = 12, BucketTypeYParam = 120, BucketTypeZParam = new DateTime(2010,6,1) }, - new List(), - new List - { - new BucketMovement { MovementDate = new DateTime(2009,7,1), Amount = 10 }, - new BucketMovement { MovementDate = new DateTime(2009,8,1), Amount = 10 }, - new BucketMovement { MovementDate = new DateTime(2009,11,1), Amount = 10 } - }, - 15, 0, 0, 30, "120 until 2010-06", 25 + new BucketMovement { MovementDate = new DateTime(2009,11,1), Amount = 12.34m }, + new BucketMovement { MovementDate = new DateTime(2009,12,1), Amount = 56.78m } }, - new object[] + 30.88m, 0, 0, 69.12m, "100 until 2010-01", 69 + }, + new object[] + { + new Bucket { Name = "120 every 12 months, last 6 months, with expenses", ValidFrom = new DateTime(2009,7,1) }, + new BucketVersion { Version = 1, BucketType = 3, BucketTypeXParam = 12, BucketTypeYParam = 120, BucketTypeZParam = new DateTime(2010,6,1) }, + new List { - new Bucket { Name = "100 every 3 months, with Want", ValidFrom = new DateTime(2010,1,1) }, - new BucketVersion { Version = 1, BucketType = 3, BucketTypeXParam = 3, BucketTypeYParam = 100, BucketTypeZParam = new DateTime(2010,3,1) }, - new List(), - new List(), - 33.33m, 0, 0, 0, "100 until 2010-03", 0 + new BankTransaction { TransactionDate = new DateTime(2009,9,2), Amount = -30 }, + new BankTransaction { TransactionDate = new DateTime(2010,1,2), Amount = -10 } }, - new object[] - { - new Bucket { Name = "100 every 3 months, last month, with Want", ValidFrom = new DateTime(2009,11,1) }, - new BucketVersion { Version = 1, BucketType = 3, BucketTypeXParam = 3, BucketTypeYParam = 100, BucketTypeZParam = new DateTime(2010,1,1) }, - new List(), - new List - { - new BucketMovement { MovementDate = new DateTime(2009,11,1), Amount = 33.33m }, - new BucketMovement { MovementDate = new DateTime(2009,12,1), Amount = 33.33m } - }, - 33.34m, 0, 0, 66.66m, "100 until 2010-01", 67 + new List + { + new BucketMovement { MovementDate = new DateTime(2009,7,1), Amount = 10 }, + new BucketMovement { MovementDate = new DateTime(2009,8,1), Amount = 10 }, + new BucketMovement { MovementDate = new DateTime(2009,9,1), Amount = 10 }, + new BucketMovement { MovementDate = new DateTime(2009,10,1), Amount = 10 }, + new BucketMovement { MovementDate = new DateTime(2009,11,1), Amount = 10 }, + new BucketMovement { MovementDate = new DateTime(2009,12,1), Amount = 10 } }, - new object[] + 16.67m, 0, -10, 20, "120 until 2010-06", 17 + }, + new object[] + { + new Bucket { Name = "120 every 12 months, 2nd year, last 6 months, with Want", ValidFrom = new DateTime(2008,7,1) }, + new BucketVersion { Version = 1, BucketType = 3, BucketTypeXParam = 12, BucketTypeYParam = 120, BucketTypeZParam = new DateTime(2009,6,1) }, + new List { - new Bucket { Name = "100 every 3 months, last month, input not in sync", ValidFrom = new DateTime(2009,11,1) }, - new BucketVersion { Version = 1, BucketType = 3, BucketTypeXParam = 3, BucketTypeYParam = 100, BucketTypeZParam = new DateTime(2010,1,1) }, - new List(), - new List - { - new BucketMovement { MovementDate = new DateTime(2009,11,1), Amount = 12.34m }, - new BucketMovement { MovementDate = new DateTime(2009,12,1), Amount = 56.78m } - }, - 30.88m, 0, 0, 69.12m, "100 until 2010-01", 69 + new BankTransaction { TransactionDate = new DateTime(2009,6,1), Amount = -120 } }, - new object[] - { - new Bucket { Name = "120 every 12 months, last 6 months, with expenses", ValidFrom = new DateTime(2009,7,1) }, - new BucketVersion { Version = 1, BucketType = 3, BucketTypeXParam = 12, BucketTypeYParam = 120, BucketTypeZParam = new DateTime(2010,6,1) }, - new List - { - new BankTransaction { TransactionDate = new DateTime(2009,9,2), Amount = -30 }, - new BankTransaction { TransactionDate = new DateTime(2010,1,2), Amount = -10 } - }, - new List - { - new BucketMovement { MovementDate = new DateTime(2009,7,1), Amount = 10 }, - new BucketMovement { MovementDate = new DateTime(2009,8,1), Amount = 10 }, - new BucketMovement { MovementDate = new DateTime(2009,9,1), Amount = 10 }, - new BucketMovement { MovementDate = new DateTime(2009,10,1), Amount = 10 }, - new BucketMovement { MovementDate = new DateTime(2009,11,1), Amount = 10 }, - new BucketMovement { MovementDate = new DateTime(2009,12,1), Amount = 10 } - }, - 16.67m, 0, -10, 20, "120 until 2010-06", 17 + new List + { + new BucketMovement { MovementDate = new DateTime(2008,7,1), Amount = 10 }, + new BucketMovement { MovementDate = new DateTime(2008,8,1), Amount = 10 }, + new BucketMovement { MovementDate = new DateTime(2008,9,1), Amount = 10 }, + new BucketMovement { MovementDate = new DateTime(2008,10,1), Amount = 10 }, + new BucketMovement { MovementDate = new DateTime(2008,11,1), Amount = 10 }, + new BucketMovement { MovementDate = new DateTime(2008,12,1), Amount = 10 }, + new BucketMovement { MovementDate = new DateTime(2009,1,1), Amount = 10 }, + new BucketMovement { MovementDate = new DateTime(2009,2,1), Amount = 10 }, + new BucketMovement { MovementDate = new DateTime(2009,3,1), Amount = 10 }, + new BucketMovement { MovementDate = new DateTime(2009,4,1), Amount = 10 }, + new BucketMovement { MovementDate = new DateTime(2009,5,1), Amount = 10 }, + new BucketMovement { MovementDate = new DateTime(2009,6,1), Amount = 10 }, + new BucketMovement { MovementDate = new DateTime(2009,7,1), Amount = 10 }, + new BucketMovement { MovementDate = new DateTime(2009,8,1), Amount = 10 }, + new BucketMovement { MovementDate = new DateTime(2009,9,1), Amount = 10 }, + new BucketMovement { MovementDate = new DateTime(2009,10,1), Amount = 10 }, + new BucketMovement { MovementDate = new DateTime(2009,11,1), Amount = 10 }, + new BucketMovement { MovementDate = new DateTime(2009,12,1), Amount = 10 } }, - new object[] - { - new Bucket { Name = "120 every 12 months, 2nd year, last 6 months, with Want", ValidFrom = new DateTime(2008,7,1) }, - new BucketVersion { Version = 1, BucketType = 3, BucketTypeXParam = 12, BucketTypeYParam = 120, BucketTypeZParam = new DateTime(2009,6,1) }, - new List - { - new BankTransaction { TransactionDate = new DateTime(2009,6,1), Amount = -120 } - }, - new List - { - new BucketMovement { MovementDate = new DateTime(2008,7,1), Amount = 10 }, - new BucketMovement { MovementDate = new DateTime(2008,8,1), Amount = 10 }, - new BucketMovement { MovementDate = new DateTime(2008,9,1), Amount = 10 }, - new BucketMovement { MovementDate = new DateTime(2008,10,1), Amount = 10 }, - new BucketMovement { MovementDate = new DateTime(2008,11,1), Amount = 10 }, - new BucketMovement { MovementDate = new DateTime(2008,12,1), Amount = 10 }, - new BucketMovement { MovementDate = new DateTime(2009,1,1), Amount = 10 }, - new BucketMovement { MovementDate = new DateTime(2009,2,1), Amount = 10 }, - new BucketMovement { MovementDate = new DateTime(2009,3,1), Amount = 10 }, - new BucketMovement { MovementDate = new DateTime(2009,4,1), Amount = 10 }, - new BucketMovement { MovementDate = new DateTime(2009,5,1), Amount = 10 }, - new BucketMovement { MovementDate = new DateTime(2009,6,1), Amount = 10 }, - new BucketMovement { MovementDate = new DateTime(2009,7,1), Amount = 10 }, - new BucketMovement { MovementDate = new DateTime(2009,8,1), Amount = 10 }, - new BucketMovement { MovementDate = new DateTime(2009,9,1), Amount = 10 }, - new BucketMovement { MovementDate = new DateTime(2009,10,1), Amount = 10 }, - new BucketMovement { MovementDate = new DateTime(2009,11,1), Amount = 10 }, - new BucketMovement { MovementDate = new DateTime(2009,12,1), Amount = 10 } - }, - 10, 0, 0, 60, "120 until 2010-06", 50 - } - }; - } + 10, 0, 0, 60, "120 until 2010-06", 50 + } + }; } + } - public static IEnumerable TestData_CheckWantAndDetailCalculation_SaveXUntilY + public static IEnumerable TestData_CheckWantAndDetailCalculation_SaveXUntilY + { + get { - get + return new[] { - return new[] + new object[] { - new object[] + new Bucket { Name = "120 until 2010-12, no input", ValidFrom = new DateTime(2010,1,1) }, + new BucketVersion { Version = 1, BucketType = 4, BucketTypeYParam = 120, BucketTypeZParam = new DateTime(2010,12,1) }, + new List(), + new List(), + 10, 0, 0, 0, "120 until 2010-12", 0 + }, + new object[] + { + new Bucket { Name = "120 until 2010-12, input in current Month", ValidFrom = new DateTime(2010,1,1) }, + new BucketVersion { Version = 1, BucketType = 4, BucketTypeYParam = 120, BucketTypeZParam = new DateTime(2010,12,1) }, + new List(), + new List { - new Bucket { Name = "120 until 2010-12, no input", ValidFrom = new DateTime(2010,1,1) }, - new BucketVersion { Version = 1, BucketType = 4, BucketTypeYParam = 120, BucketTypeZParam = new DateTime(2010,12,1) }, - new List(), - new List(), - 10, 0, 0, 0, "120 until 2010-12", 0 + new BucketMovement { MovementDate = new DateTime(2010,1,1), Amount = 10 } }, - new object[] - { - new Bucket { Name = "120 until 2010-12, input in current Month", ValidFrom = new DateTime(2010,1,1) }, - new BucketVersion { Version = 1, BucketType = 4, BucketTypeYParam = 120, BucketTypeZParam = new DateTime(2010,12,1) }, - new List(), - new List - { - new BucketMovement { MovementDate = new DateTime(2010,1,1), Amount = 10 } - }, - 0, 10, 0, 10, "120 until 2010-12", 8 + 0, 10, 0, 10, "120 until 2010-12", 8 + }, + new object[] + { + new Bucket { Name = "120 until 2010-06, input in sync", ValidFrom = new DateTime(2009,7,1) }, + new BucketVersion { Version = 1, BucketType = 4, BucketTypeYParam = 120, BucketTypeZParam = new DateTime(2010,6,1) }, + new List(), + new List + { + new BucketMovement { MovementDate = new DateTime(2009,7,1), Amount = 10 }, + new BucketMovement { MovementDate = new DateTime(2009,8,1), Amount = 10 }, + new BucketMovement { MovementDate = new DateTime(2009,9,1), Amount = 10 }, + new BucketMovement { MovementDate = new DateTime(2009,10,1), Amount = 10 }, + new BucketMovement { MovementDate = new DateTime(2009,11,1), Amount = 10 }, + new BucketMovement { MovementDate = new DateTime(2009,12,1), Amount = 10 } }, - new object[] - { - new Bucket { Name = "120 until 2010-06, input in sync", ValidFrom = new DateTime(2009,7,1) }, - new BucketVersion { Version = 1, BucketType = 4, BucketTypeYParam = 120, BucketTypeZParam = new DateTime(2010,6,1) }, - new List(), - new List - { - new BucketMovement { MovementDate = new DateTime(2009,7,1), Amount = 10 }, - new BucketMovement { MovementDate = new DateTime(2009,8,1), Amount = 10 }, - new BucketMovement { MovementDate = new DateTime(2009,9,1), Amount = 10 }, - new BucketMovement { MovementDate = new DateTime(2009,10,1), Amount = 10 }, - new BucketMovement { MovementDate = new DateTime(2009,11,1), Amount = 10 }, - new BucketMovement { MovementDate = new DateTime(2009,12,1), Amount = 10 } - }, - 10, 0, 0, 60, "120 until 2010-06", 50 + 10, 0, 0, 60, "120 until 2010-06", 50 + }, + new object[] + { + new Bucket { Name = "120 until 2010-06, fulfilled target", ValidFrom = new DateTime(2009,7,1) }, + new BucketVersion { Version = 1, BucketType = 4, BucketTypeYParam = 120, BucketTypeZParam = new DateTime(2010,6,1) }, + new List(), + new List + { + new BucketMovement { MovementDate = new DateTime(2009,7,1), Amount = 20 }, + new BucketMovement { MovementDate = new DateTime(2009,8,1), Amount = 20 }, + new BucketMovement { MovementDate = new DateTime(2009,9,1), Amount = 20 }, + new BucketMovement { MovementDate = new DateTime(2009,10,1), Amount = 20 }, + new BucketMovement { MovementDate = new DateTime(2009,11,1), Amount = 20 }, + new BucketMovement { MovementDate = new DateTime(2009,12,1), Amount = 20 } }, - new object[] - { - new Bucket { Name = "120 until 2010-06, fulfilled target", ValidFrom = new DateTime(2009,7,1) }, - new BucketVersion { Version = 1, BucketType = 4, BucketTypeYParam = 120, BucketTypeZParam = new DateTime(2010,6,1) }, - new List(), - new List - { - new BucketMovement { MovementDate = new DateTime(2009,7,1), Amount = 20 }, - new BucketMovement { MovementDate = new DateTime(2009,8,1), Amount = 20 }, - new BucketMovement { MovementDate = new DateTime(2009,9,1), Amount = 20 }, - new BucketMovement { MovementDate = new DateTime(2009,10,1), Amount = 20 }, - new BucketMovement { MovementDate = new DateTime(2009,11,1), Amount = 20 }, - new BucketMovement { MovementDate = new DateTime(2009,12,1), Amount = 20 } - }, - 0, 0, 0, 120, "120 until 2010-06", 100 + 0, 0, 0, 120, "120 until 2010-06", 100 + }, + new object[] + { + new Bucket { Name = "120 until 2010-06, over-fulfilled target", ValidFrom = new DateTime(2009,7,1) }, + new BucketVersion { Version = 1, BucketType = 4, BucketTypeYParam = 120, BucketTypeZParam = new DateTime(2010,6,1) }, + new List(), + new List + { + new BucketMovement { MovementDate = new DateTime(2009,7,1), Amount = 20 }, + new BucketMovement { MovementDate = new DateTime(2009,8,1), Amount = 20 }, + new BucketMovement { MovementDate = new DateTime(2009,9,1), Amount = 20 }, + new BucketMovement { MovementDate = new DateTime(2009,10,1), Amount = 20 }, + new BucketMovement { MovementDate = new DateTime(2009,11,1), Amount = 20 }, + new BucketMovement { MovementDate = new DateTime(2009,12,1), Amount = 30 } + }, + 0, 0, 0, 130, "120 until 2010-06", 100 + }, + new object[] + { + new Bucket { Name = "120 until 2010-06, input not in sync", ValidFrom = new DateTime(2009,7,1) }, + new BucketVersion { Version = 1, BucketType = 4, BucketTypeYParam = 120, BucketTypeZParam = new DateTime(2010,6,1) }, + new List(), + new List + { + new BucketMovement { MovementDate = new DateTime(2009,7,1), Amount = 10 }, + new BucketMovement { MovementDate = new DateTime(2009,9,1), Amount = 10 }, + new BucketMovement { MovementDate = new DateTime(2009,10,1), Amount = 10 } }, - new object[] + 15, 0, 0, 30, "120 until 2010-06", 25 + }, + new object[] + { + new Bucket { Name = "120 until 2009-12, target not reached", ValidFrom = new DateTime(2009,7,1) }, + new BucketVersion { Version = 1, BucketType = 4, BucketTypeYParam = 120, BucketTypeZParam = new DateTime(2009,12,1) }, + new List(), + new List + { + new BucketMovement { MovementDate = new DateTime(2009,7,1), Amount = 10 }, + new BucketMovement { MovementDate = new DateTime(2009,9,1), Amount = 10 }, + new BucketMovement { MovementDate = new DateTime(2009,10,1), Amount = 10 } + }, + 0, 0, 0, 30, "120 until 2009-12", 25 + }, + new object[] + { + new Bucket { Name = "30 until 2010-01, target reached, with expense in target month", ValidFrom = new DateTime(2009,7,1) }, + new BucketVersion { Version = 1, BucketType = 4, BucketTypeYParam = 30, BucketTypeZParam = new DateTime(2010,1,1) }, + new List { - new Bucket { Name = "120 until 2010-06, over-fulfilled target", ValidFrom = new DateTime(2009,7,1) }, - new BucketVersion { Version = 1, BucketType = 4, BucketTypeYParam = 120, BucketTypeZParam = new DateTime(2010,6,1) }, - new List(), - new List - { - new BucketMovement { MovementDate = new DateTime(2009,7,1), Amount = 20 }, - new BucketMovement { MovementDate = new DateTime(2009,8,1), Amount = 20 }, - new BucketMovement { MovementDate = new DateTime(2009,9,1), Amount = 20 }, - new BucketMovement { MovementDate = new DateTime(2009,10,1), Amount = 20 }, - new BucketMovement { MovementDate = new DateTime(2009,11,1), Amount = 20 }, - new BucketMovement { MovementDate = new DateTime(2009,12,1), Amount = 30 } - }, - 0, 0, 0, 130, "120 until 2010-06", 100 + new BankTransaction { TransactionDate = new DateTime(2010,1,5), Amount = -30 } }, - new object[] + new List { - new Bucket { Name = "120 until 2010-06, input not in sync", ValidFrom = new DateTime(2009,7,1) }, - new BucketVersion { Version = 1, BucketType = 4, BucketTypeYParam = 120, BucketTypeZParam = new DateTime(2010,6,1) }, - new List(), - new List - { - new BucketMovement { MovementDate = new DateTime(2009,7,1), Amount = 10 }, - new BucketMovement { MovementDate = new DateTime(2009,9,1), Amount = 10 }, - new BucketMovement { MovementDate = new DateTime(2009,10,1), Amount = 10 } - }, - 15, 0, 0, 30, "120 until 2010-06", 25 + new BucketMovement { MovementDate = new DateTime(2009,7,1), Amount = 10 }, + new BucketMovement { MovementDate = new DateTime(2009,9,1), Amount = 10 }, + new BucketMovement { MovementDate = new DateTime(2009,10,1), Amount = 10 } }, - new object[] + 0, 0, -30, 0, "30 until 2010-01", 100 + }, + new object[] + { + new Bucket { Name = "30 until 2010-01, target reached, with lower expense in target month", ValidFrom = new DateTime(2009,7,1) }, + new BucketVersion { Version = 1, BucketType = 4, BucketTypeYParam = 30, BucketTypeZParam = new DateTime(2010,1,1) }, + new List { - new Bucket { Name = "120 until 2009-12, target not reached", ValidFrom = new DateTime(2009,7,1) }, - new BucketVersion { Version = 1, BucketType = 4, BucketTypeYParam = 120, BucketTypeZParam = new DateTime(2009,12,1) }, - new List(), - new List - { - new BucketMovement { MovementDate = new DateTime(2009,7,1), Amount = 10 }, - new BucketMovement { MovementDate = new DateTime(2009,9,1), Amount = 10 }, - new BucketMovement { MovementDate = new DateTime(2009,10,1), Amount = 10 } - }, - 0, 0, 0, 30, "120 until 2009-12", 25 + new BankTransaction { TransactionDate = new DateTime(2010,1,5), Amount = -20 } }, - new object[] + new List { - new Bucket { Name = "30 until 2010-01, target reached, with expense in target month", ValidFrom = new DateTime(2009,7,1) }, - new BucketVersion { Version = 1, BucketType = 4, BucketTypeYParam = 30, BucketTypeZParam = new DateTime(2010,1,1) }, - new List - { - new BankTransaction { TransactionDate = new DateTime(2010,1,5), Amount = -30 } - }, - new List - { - new BucketMovement { MovementDate = new DateTime(2009,7,1), Amount = 10 }, - new BucketMovement { MovementDate = new DateTime(2009,9,1), Amount = 10 }, - new BucketMovement { MovementDate = new DateTime(2009,10,1), Amount = 10 } - }, - 0, 0, -30, 0, "30 until 2010-01", 100 + new BucketMovement { MovementDate = new DateTime(2009,7,1), Amount = 10 }, + new BucketMovement { MovementDate = new DateTime(2009,9,1), Amount = 10 }, + new BucketMovement { MovementDate = new DateTime(2009,10,1), Amount = 10 } }, - new object[] + 0, 0, -20, 10, "30 until 2010-01", 100 + }, + new object[] + { + new Bucket { Name = "30 until 2010-01, target reached, with higher expense in target month", ValidFrom = new DateTime(2009,7,1) }, + new BucketVersion { Version = 1, BucketType = 4, BucketTypeYParam = 30, BucketTypeZParam = new DateTime(2010,1,1) }, + new List { - new Bucket { Name = "30 until 2010-01, target reached, with lower expense in target month", ValidFrom = new DateTime(2009,7,1) }, - new BucketVersion { Version = 1, BucketType = 4, BucketTypeYParam = 30, BucketTypeZParam = new DateTime(2010,1,1) }, - new List - { - new BankTransaction { TransactionDate = new DateTime(2010,1,5), Amount = -20 } - }, - new List - { - new BucketMovement { MovementDate = new DateTime(2009,7,1), Amount = 10 }, - new BucketMovement { MovementDate = new DateTime(2009,9,1), Amount = 10 }, - new BucketMovement { MovementDate = new DateTime(2009,10,1), Amount = 10 } - }, - 0, 0, -20, 10, "30 until 2010-01", 100 + new BankTransaction { TransactionDate = new DateTime(2010,1,5), Amount = -40 } }, - new object[] + new List { - new Bucket { Name = "30 until 2010-01, target reached, with higher expense in target month", ValidFrom = new DateTime(2009,7,1) }, - new BucketVersion { Version = 1, BucketType = 4, BucketTypeYParam = 30, BucketTypeZParam = new DateTime(2010,1,1) }, - new List - { - new BankTransaction { TransactionDate = new DateTime(2010,1,5), Amount = -40 } - }, - new List - { - new BucketMovement { MovementDate = new DateTime(2009,7,1), Amount = 10 }, - new BucketMovement { MovementDate = new DateTime(2009,9,1), Amount = 10 }, - new BucketMovement { MovementDate = new DateTime(2009,10,1), Amount = 10 } - }, - 10, 0, -40, -10, "30 until 2010-01", 75 - } - }; - } + new BucketMovement { MovementDate = new DateTime(2009,7,1), Amount = 10 }, + new BucketMovement { MovementDate = new DateTime(2009,9,1), Amount = 10 }, + new BucketMovement { MovementDate = new DateTime(2009,10,1), Amount = 10 } + }, + 10, 0, -40, -10, "30 until 2010-01", 75 + } + }; } + } - [Theory] - [MemberData(nameof(TestData_CheckWantAndDetailCalculation_ExpenseEveryXMonths))] - [MemberData(nameof(TestData_CheckWantAndDetailCalculation_SaveXUntilY))] - public async Task LoadDataAsync_CheckWantAndDetailCalculation_ExpenseEveryXMonths_SaveXUntilY( - Bucket testBucket, - BucketVersion testBucketVersion, - List testTransactions, - List testBucketMovements, - decimal expectedWant, - decimal expectedIn, - decimal expectedActivity, - decimal expectedBalance, - string expectedDetails, - int expectedProgress - ) + [Theory] + [MemberData(nameof(TestData_CheckWantAndDetailCalculation_ExpenseEveryXMonths))] + [MemberData(nameof(TestData_CheckWantAndDetailCalculation_SaveXUntilY))] + public async Task LoadDataAsync_CheckWantAndDetailCalculation_ExpenseEveryXMonths_SaveXUntilY( + Bucket testBucket, + BucketVersion testBucketVersion, + List testTransactions, + List testBucketMovements, + decimal expectedWant, + decimal expectedIn, + decimal expectedActivity, + decimal expectedBalance, + string expectedDetails, + int expectedProgress + ) + { + try { - try - { - var testObject = await ExecuteBucketCreationAndTransactionMovementsAsync( - testBucket, testBucketVersion, testTransactions, testBucketMovements, new DateTime(2010,1,1)); - - Assert.Equal(expectedWant, testObject.Want); - Assert.Equal(expectedIn, testObject.In); - Assert.Equal(expectedActivity, testObject.Activity); - Assert.Equal(expectedBalance, testObject.Balance); - Assert.Equal(expectedDetails, testObject.Details); - Assert.Equal(expectedProgress, testObject.Progress); - } - finally - { - DbConnector.CleanupDatabase(nameof(BucketViewModelTest)); - } + var testObject = await ExecuteBucketCreationAndTransactionMovementsAsync( + testBucket, testBucketVersion, testTransactions, testBucketMovements, new DateTime(2010,1,1)); + + Assert.Equal(expectedWant, testObject.Want); + Assert.Equal(expectedIn, testObject.In); + Assert.Equal(expectedActivity, testObject.Activity); + Assert.Equal(expectedBalance, testObject.Balance); + Assert.Equal(expectedDetails, testObject.Details); + Assert.Equal(expectedProgress, testObject.Progress); } + finally + { + DbConnector.CleanupDatabase(nameof(BucketViewModelTest)); + } + } - private async Task ExecuteBucketCreationAndTransactionMovementsAsync( - Bucket testBucket, - BucketVersion testBucketVersion, - IEnumerable testTransactions, - IEnumerable testBucketMovements, - DateTime testMonth) + private async Task ExecuteBucketCreationAndTransactionMovementsAsync( + Bucket testBucket, + BucketVersion testBucketVersion, + IEnumerable testTransactions, + IEnumerable testBucketMovements, + DateTime testMonth) + { + using (var dbContext = new DatabaseContext(_dbOptions)) { - using (var dbContext = new DatabaseContext(_dbOptions)) - { - var testAccount = new Account() { IsActive = 1, Name = "Account" }; - var testBucketGroup = new BucketGroup() { Name = "Bucket Group", Position = 1 }; + var testAccount = new Account() { IsActive = 1, Name = "Account" }; + var testBucketGroup = new BucketGroup() { Name = "Bucket Group", Position = 1 }; - dbContext.CreateAccount(testAccount); - dbContext.CreateBucketGroup(testBucketGroup); + dbContext.CreateAccount(testAccount); + dbContext.CreateBucketGroup(testBucketGroup); - testBucket.BucketGroupId = testBucketGroup.BucketGroupId; - dbContext.CreateBucket(testBucket); + testBucket.BucketGroupId = testBucketGroup.BucketGroupId; + dbContext.CreateBucket(testBucket); - testBucketVersion.BucketId = testBucket.BucketId; - testBucketVersion.ValidFrom = testBucket.ValidFrom; - dbContext.CreateBucketVersion(testBucketVersion); + testBucketVersion.BucketId = testBucket.BucketId; + testBucketVersion.ValidFrom = testBucket.ValidFrom; + dbContext.CreateBucketVersion(testBucketVersion); - foreach (var testTransaction in testTransactions) - { - testTransaction.AccountId = testAccount.AccountId; - dbContext.CreateBankTransaction(testTransaction); - dbContext.CreateBudgetedTransaction(new BudgetedTransaction() - { - TransactionId = testTransaction.TransactionId, - BucketId = testBucket.BucketId, - Amount = testTransaction.Amount - }); - } - - foreach (var testBucketMovement in testBucketMovements) - { - testBucketMovement.BucketId = testBucket.BucketId; - dbContext.CreateBucketMovement(testBucketMovement); - } - - var monthSelectorViewModel = new YearMonthSelectorViewModel() + foreach (var testTransaction in testTransactions) + { + testTransaction.AccountId = testAccount.AccountId; + dbContext.CreateBankTransaction(testTransaction); + dbContext.CreateBudgetedTransaction(new BudgetedTransaction() { - SelectedYear = testMonth.Year, - SelectedMonth = testMonth.Month - }; - var viewModel = new BucketViewModel(_dbOptions, monthSelectorViewModel); - await viewModel.LoadDataAsync(); + TransactionId = testTransaction.TransactionId, + BucketId = testBucket.BucketId, + Amount = testTransaction.Amount + }); + } + + foreach (var testBucketMovement in testBucketMovements) + { + testBucketMovement.BucketId = testBucket.BucketId; + dbContext.CreateBucketMovement(testBucketMovement); + } + + var monthSelectorViewModel = new YearMonthSelectorViewModel() + { + SelectedYear = testMonth.Year, + SelectedMonth = testMonth.Month + }; + var viewModel = new BucketViewModel(_dbOptions, monthSelectorViewModel); + await viewModel.LoadDataAsync(); - var bucketGroup = viewModel.BucketGroups.FirstOrDefault(i => i.BucketGroup.BucketGroupId == testBucketGroup.BucketGroupId); - Assert.NotNull(bucketGroup); + var bucketGroup = viewModel.BucketGroups.FirstOrDefault(i => i.BucketGroup.BucketGroupId == testBucketGroup.BucketGroupId); + Assert.NotNull(bucketGroup); - var testObject = bucketGroup.Buckets.FirstOrDefault(i => i.Bucket.BucketId == testBucket.BucketId); - Assert.NotNull(testObject); + var testObject = bucketGroup.Buckets.FirstOrDefault(i => i.Bucket.BucketId == testBucket.BucketId); + Assert.NotNull(testObject); - return testObject; - } + return testObject; } + } - public async Task DistributeBudget_CheckDistributedMoney( - IEnumerable> testBuckets - ) + public async Task DistributeBudget_CheckDistributedMoney( + IEnumerable> testBuckets + ) + { + using (var dbContext = new DatabaseContext(_dbOptions)) { - using (var dbContext = new DatabaseContext(_dbOptions)) - { - var testAccount = new Account() { IsActive = 1, Name = "Account" }; - var testBucketGroup = new BucketGroup() { Name = "Bucket Group", Position = 1 }; + var testAccount = new Account() { IsActive = 1, Name = "Account" }; + var testBucketGroup = new BucketGroup() { Name = "Bucket Group", Position = 1 }; - dbContext.CreateAccount(testAccount); - dbContext.CreateBucketGroup(testBucketGroup); + dbContext.CreateAccount(testAccount); + dbContext.CreateBucketGroup(testBucketGroup); - foreach (var (bucket, bucketVersion) in testBuckets) - { - bucket.BucketGroupId = testBucketGroup.BucketGroupId; - dbContext.CreateBucket(bucket); - bucketVersion.BucketId = bucket.BucketId; - dbContext.CreateBucketVersion(bucketVersion); - } + foreach (var (bucket, bucketVersion) in testBuckets) + { + bucket.BucketGroupId = testBucketGroup.BucketGroupId; + dbContext.CreateBucket(bucket); + bucketVersion.BucketId = bucket.BucketId; + dbContext.CreateBucketVersion(bucketVersion); } } } diff --git a/OpenBudgeteer.Core.Test/ViewModelTest/YearMonthSelectorViewModelTest.cs b/OpenBudgeteer.Core.Test/ViewModelTest/YearMonthSelectorViewModelTest.cs index c412bd6..3318a4a 100644 --- a/OpenBudgeteer.Core.Test/ViewModelTest/YearMonthSelectorViewModelTest.cs +++ b/OpenBudgeteer.Core.Test/ViewModelTest/YearMonthSelectorViewModelTest.cs @@ -6,130 +6,129 @@ using Xunit; using Xunit.Abstractions; -namespace OpenBudgeteer.Core.Test.ViewModelTest +namespace OpenBudgeteer.Core.Test.ViewModelTest; + +public class YearMonthSelectorViewModelTest { - public class YearMonthSelectorViewModelTest + private readonly ITestOutputHelper _output; + + public YearMonthSelectorViewModelTest(ITestOutputHelper output) { - private readonly ITestOutputHelper _output; + _output = output ?? throw new ArgumentNullException(nameof(output)); + } - public YearMonthSelectorViewModelTest(ITestOutputHelper output) - { - _output = output ?? throw new ArgumentNullException(nameof(output)); - } + [Fact] + public void Constructor_CheckDefaults() + { + var viewModel = new YearMonthSelectorViewModel(); - [Fact] - public void Constructor_CheckDefaults() + Assert.Equal(DateTime.Now.Year, viewModel.SelectedYear); + Assert.Equal(DateTime.Now.Month, viewModel.SelectedMonth); + Assert.Equal(new DateTime(DateTime.Now.Year, DateTime.Now.Month, 1), viewModel.CurrentMonth); + + // Test Months + Assert.Equal(12, viewModel.Months.Count); + for (int i = 1; i < 13; i++) { - var viewModel = new YearMonthSelectorViewModel(); - - Assert.Equal(DateTime.Now.Year, viewModel.SelectedYear); - Assert.Equal(DateTime.Now.Month, viewModel.SelectedMonth); - Assert.Equal(new DateTime(DateTime.Now.Year, DateTime.Now.Month, 1), viewModel.CurrentMonth); - - // Test Months - Assert.Equal(12, viewModel.Months.Count); - for (int i = 1; i < 13; i++) - { - Assert.Equal(i, viewModel.Months.ElementAt(i-1)); - } - - var cultureInfo = new CultureInfo("de-DE"); - - _output.WriteLine($"Current Culture: {CultureInfo.CurrentCulture.Name}"); - _output.WriteLine($"Current UI Culture: {CultureInfo.CurrentUICulture.Name}"); - _output.WriteLine($"Test culture: {cultureInfo.Name}"); - - var converter = new MonthOutputConverter(); - - Assert.Equal("Jan", converter.ConvertMonth(1, cultureInfo)); - Assert.Equal("Feb", converter.ConvertMonth(2, cultureInfo)); - Assert.Equal("Mär", converter.ConvertMonth(3, cultureInfo)); - Assert.Equal("Apr", converter.ConvertMonth(4, cultureInfo)); - Assert.Equal("Mai", converter.ConvertMonth(5, cultureInfo)); - Assert.Equal("Jun", converter.ConvertMonth(6, cultureInfo)); - Assert.Equal("Jul", converter.ConvertMonth(7, cultureInfo)); - Assert.Equal("Aug", converter.ConvertMonth(8, cultureInfo)); - Assert.Equal("Sep", converter.ConvertMonth(9, cultureInfo)); - Assert.Equal("Okt", converter.ConvertMonth(10, cultureInfo)); - Assert.Equal("Nov", converter.ConvertMonth(11, cultureInfo)); - Assert.Equal("Dez", converter.ConvertMonth(12, cultureInfo)); + Assert.Equal(i, viewModel.Months.ElementAt(i-1)); } + + var cultureInfo = new CultureInfo("de-DE"); + + _output.WriteLine($"Current Culture: {CultureInfo.CurrentCulture.Name}"); + _output.WriteLine($"Current UI Culture: {CultureInfo.CurrentUICulture.Name}"); + _output.WriteLine($"Test culture: {cultureInfo.Name}"); + + var converter = new MonthOutputConverter(); + + Assert.Equal("Jan", converter.ConvertMonth(1, cultureInfo)); + Assert.Equal("Feb", converter.ConvertMonth(2, cultureInfo)); + Assert.Equal("Mär", converter.ConvertMonth(3, cultureInfo)); + Assert.Equal("Apr", converter.ConvertMonth(4, cultureInfo)); + Assert.Equal("Mai", converter.ConvertMonth(5, cultureInfo)); + Assert.Equal("Jun", converter.ConvertMonth(6, cultureInfo)); + Assert.Equal("Jul", converter.ConvertMonth(7, cultureInfo)); + Assert.Equal("Aug", converter.ConvertMonth(8, cultureInfo)); + Assert.Equal("Sep", converter.ConvertMonth(9, cultureInfo)); + Assert.Equal("Okt", converter.ConvertMonth(10, cultureInfo)); + Assert.Equal("Nov", converter.ConvertMonth(11, cultureInfo)); + Assert.Equal("Dez", converter.ConvertMonth(12, cultureInfo)); + } - [Fact] - public void PreviousMonth_CheckMonth() + [Fact] + public void PreviousMonth_CheckMonth() + { + var viewModel = new YearMonthSelectorViewModel() { - var viewModel = new YearMonthSelectorViewModel() - { - SelectedYear = 2010, - SelectedMonth = 2 - }; + SelectedYear = 2010, + SelectedMonth = 2 + }; - viewModel.PreviousMonth(); + viewModel.PreviousMonth(); - Assert.Equal(2010, viewModel.SelectedYear); - Assert.Equal(1, viewModel.SelectedMonth); + Assert.Equal(2010, viewModel.SelectedYear); + Assert.Equal(1, viewModel.SelectedMonth); - viewModel.PreviousMonth(); + viewModel.PreviousMonth(); - Assert.Equal(2009, viewModel.SelectedYear); - Assert.Equal(12, viewModel.SelectedMonth); - } + Assert.Equal(2009, viewModel.SelectedYear); + Assert.Equal(12, viewModel.SelectedMonth); + } - [Fact] - public void NextMonth_CheckMonth() + [Fact] + public void NextMonth_CheckMonth() + { + var viewModel = new YearMonthSelectorViewModel() { - var viewModel = new YearMonthSelectorViewModel() - { - SelectedYear = 2009, - SelectedMonth = 11 - }; + SelectedYear = 2009, + SelectedMonth = 11 + }; - viewModel.NextMonth(); + viewModel.NextMonth(); - Assert.Equal(2009, viewModel.SelectedYear); - Assert.Equal(12, viewModel.SelectedMonth); + Assert.Equal(2009, viewModel.SelectedYear); + Assert.Equal(12, viewModel.SelectedMonth); - viewModel.NextMonth(); + viewModel.NextMonth(); - Assert.Equal(2010, viewModel.SelectedYear); - Assert.Equal(1, viewModel.SelectedMonth); - } + Assert.Equal(2010, viewModel.SelectedYear); + Assert.Equal(1, viewModel.SelectedMonth); + } - [Fact] - public void SelectedYearMonthChanged_CheckEventHasBeenInvoked() + [Fact] + public void SelectedYearMonthChanged_CheckEventHasBeenInvoked() + { + var viewModel = new YearMonthSelectorViewModel() { - var viewModel = new YearMonthSelectorViewModel() - { - SelectedYear = 2010, - SelectedMonth = 1 - }; - var eventHasBeenInvoked = false; - viewModel.SelectedYearMonthChanged += (sender, args) => eventHasBeenInvoked = true; - - viewModel.SelectedYear = 2010; - Assert.False(eventHasBeenInvoked); - - eventHasBeenInvoked = false; - viewModel.SelectedMonth = 1; - Assert.False(eventHasBeenInvoked); - - eventHasBeenInvoked = false; - viewModel.SelectedYear = 2009; - Assert.True(eventHasBeenInvoked); - - eventHasBeenInvoked = false; - viewModel.SelectedMonth = 2; - Assert.True(eventHasBeenInvoked); - - eventHasBeenInvoked = false; - viewModel.NextMonth(); - Assert.True(eventHasBeenInvoked); - - eventHasBeenInvoked = false; - viewModel.PreviousMonth(); - Assert.True(eventHasBeenInvoked); - - - } + SelectedYear = 2010, + SelectedMonth = 1 + }; + var eventHasBeenInvoked = false; + viewModel.SelectedYearMonthChanged += (sender, args) => eventHasBeenInvoked = true; + + viewModel.SelectedYear = 2010; + Assert.False(eventHasBeenInvoked); + + eventHasBeenInvoked = false; + viewModel.SelectedMonth = 1; + Assert.False(eventHasBeenInvoked); + + eventHasBeenInvoked = false; + viewModel.SelectedYear = 2009; + Assert.True(eventHasBeenInvoked); + + eventHasBeenInvoked = false; + viewModel.SelectedMonth = 2; + Assert.True(eventHasBeenInvoked); + + eventHasBeenInvoked = false; + viewModel.NextMonth(); + Assert.True(eventHasBeenInvoked); + + eventHasBeenInvoked = false; + viewModel.PreviousMonth(); + Assert.True(eventHasBeenInvoked); + + } } diff --git a/OpenBudgeteer.Core/Common/Database/DatabaseContext.cs b/OpenBudgeteer.Core/Common/Database/DatabaseContext.cs index aa8615f..266ee41 100644 --- a/OpenBudgeteer.Core/Common/Database/DatabaseContext.cs +++ b/OpenBudgeteer.Core/Common/Database/DatabaseContext.cs @@ -3,343 +3,343 @@ using Microsoft.EntityFrameworkCore; using OpenBudgeteer.Core.Models; -namespace OpenBudgeteer.Core.Common.Database +namespace OpenBudgeteer.Core.Common.Database; + +public class DatabaseContext : DbContext { - public class DatabaseContext : DbContext + public DbSet Account { get; set; } + public DbSet BankTransaction { get; set; } + public DbSet Bucket { get; set; } + public DbSet BucketGroup { get; set; } + public DbSet BucketMovement { get; set; } + public DbSet BucketVersion { get; set; } + public DbSet BudgetedTransaction { get; set; } + public DbSet ImportProfile { get; set; } + public DbSet BucketRuleSet { get; set; } + public DbSet MappingRule { get; set; } + + public DatabaseContext(DbContextOptions options) : base(options) { } + + #region Create + + public int CreateAccount(Account account) + => CreateAccounts(new List() { account }); + + public int CreateAccounts(IEnumerable accounts) { - public DbSet Account { get; set; } - public DbSet BankTransaction { get; set; } - public DbSet Bucket { get; set; } - public DbSet BucketGroup { get; set; } - public DbSet BucketMovement { get; set; } - public DbSet BucketVersion { get; set; } - public DbSet BudgetedTransaction { get; set; } - public DbSet ImportProfile { get; set; } - public DbSet BucketRuleSet { get; set; } - public DbSet MappingRule { get; set; } - - public DatabaseContext(DbContextOptions options) : base(options) { } - - #region Create - - public int CreateAccount(Account account) - => CreateAccounts(new List() { account }); - - public int CreateAccounts(IEnumerable accounts) - { - Account.AddRange(accounts); - return SaveChanges(); - } + Account.AddRange(accounts); + return SaveChanges(); + } - public int CreateBankTransaction(BankTransaction bankTransaction) - => CreateBankTransactions(new List() { bankTransaction }); + public int CreateBankTransaction(BankTransaction bankTransaction) + => CreateBankTransactions(new List() { bankTransaction }); - public int CreateBankTransactions(IEnumerable bankTransactions) - { - BankTransaction.AddRange(bankTransactions); - return SaveChanges(); - } + public int CreateBankTransactions(IEnumerable bankTransactions) + { + BankTransaction.AddRange(bankTransactions); + return SaveChanges(); + } - public int CreateBucket(Bucket bucket) - => CreateBuckets(new List() { bucket }); + public int CreateBucket(Bucket bucket) + => CreateBuckets(new List() { bucket }); - public int CreateBuckets(IEnumerable buckets) - { - Bucket.AddRange(buckets); - return SaveChanges(); - } + public int CreateBuckets(IEnumerable buckets) + { + Bucket.AddRange(buckets); + return SaveChanges(); + } - public int CreateBucketGroup(BucketGroup bucketGroup) - => CreateBucketGroups(new List() { bucketGroup }); + public int CreateBucketGroup(BucketGroup bucketGroup) + => CreateBucketGroups(new List() { bucketGroup }); - public int CreateBucketGroups(IEnumerable bucketGroups) - { - BucketGroup.AddRange(bucketGroups); - return SaveChanges(); - } + public int CreateBucketGroups(IEnumerable bucketGroups) + { + BucketGroup.AddRange(bucketGroups); + return SaveChanges(); + } - public int CreateBucketMovement(BucketMovement bucketMovement) - => CreateBucketMovements(new List() { bucketMovement }); + public int CreateBucketMovement(BucketMovement bucketMovement) + => CreateBucketMovements(new List() { bucketMovement }); - public int CreateBucketMovements(IEnumerable bucketMovements) - { - BucketMovement.AddRange(bucketMovements); - return SaveChanges(); - } + public int CreateBucketMovements(IEnumerable bucketMovements) + { + BucketMovement.AddRange(bucketMovements); + return SaveChanges(); + } - public int CreateBucketVersion(BucketVersion bucketVersion) - => CreateBucketVersions(new List() { bucketVersion }); + public int CreateBucketVersion(BucketVersion bucketVersion) + => CreateBucketVersions(new List() { bucketVersion }); - public int CreateBucketVersions(IEnumerable bucketVersions) - { - BucketVersion.AddRange(bucketVersions); - return SaveChanges(); - } + public int CreateBucketVersions(IEnumerable bucketVersions) + { + BucketVersion.AddRange(bucketVersions); + return SaveChanges(); + } - public int CreateBudgetedTransaction(BudgetedTransaction budgetedTransaction) - => CreateBudgetedTransactions(new List() { budgetedTransaction }); + public int CreateBudgetedTransaction(BudgetedTransaction budgetedTransaction) + => CreateBudgetedTransactions(new List() { budgetedTransaction }); - public int CreateBudgetedTransactions(IEnumerable budgetedTransactions) - { - BudgetedTransaction.AddRange(budgetedTransactions); - return SaveChanges(); - } + public int CreateBudgetedTransactions(IEnumerable budgetedTransactions) + { + BudgetedTransaction.AddRange(budgetedTransactions); + return SaveChanges(); + } - public int CreateImportProfile(ImportProfile importProfile) - => CreateImportProfiles(new List() { importProfile }); + public int CreateImportProfile(ImportProfile importProfile) + => CreateImportProfiles(new List() { importProfile }); - public int CreateImportProfiles(IEnumerable importProfiles) - { - ImportProfile.AddRange(importProfiles); - return SaveChanges(); - } + public int CreateImportProfiles(IEnumerable importProfiles) + { + ImportProfile.AddRange(importProfiles); + return SaveChanges(); + } - public int CreateBucketRuleSet(BucketRuleSet bucketRuleSet) - => CreateBucketRuleSets(new List() { bucketRuleSet }); + public int CreateBucketRuleSet(BucketRuleSet bucketRuleSet) + => CreateBucketRuleSets(new List() { bucketRuleSet }); - public int CreateBucketRuleSets(IEnumerable bucketRuleSets) - { - BucketRuleSet.AddRange(bucketRuleSets); - return SaveChanges(); - } + public int CreateBucketRuleSets(IEnumerable bucketRuleSets) + { + BucketRuleSet.AddRange(bucketRuleSets); + return SaveChanges(); + } - public int CreateMappingRule(MappingRule mappingRule) - => CreateMappingRules(new List() { mappingRule }); + public int CreateMappingRule(MappingRule mappingRule) + => CreateMappingRules(new List() { mappingRule }); - public int CreateMappingRules(IEnumerable mappingRules) - { - MappingRule.AddRange(mappingRules); - return SaveChanges(); - } + public int CreateMappingRules(IEnumerable mappingRules) + { + MappingRule.AddRange(mappingRules); + return SaveChanges(); + } - #endregion + #endregion - #region Update + #region Update - public int UpdateAccount(Account account) - => UpdateAccounts(new List() { account }); + public int UpdateAccount(Account account) + => UpdateAccounts(new List() { account }); - public int UpdateAccounts(IEnumerable accounts) + public int UpdateAccounts(IEnumerable accounts) + { + foreach (var account in accounts) { - foreach (var account in accounts) - { - var dbAccount = Account.First(i => i.AccountId == account.AccountId); - Entry(dbAccount).CurrentValues.SetValues(account); - } - return SaveChanges(); + var dbAccount = Account.First(i => i.AccountId == account.AccountId); + Entry(dbAccount).CurrentValues.SetValues(account); } + return SaveChanges(); + } - public int UpdateBankTransaction(BankTransaction bankTransaction) - => UpdateBankTransactions(new List() { bankTransaction }); + public int UpdateBankTransaction(BankTransaction bankTransaction) + => UpdateBankTransactions(new List() { bankTransaction }); - public int UpdateBankTransactions(IEnumerable bankTransactions) + public int UpdateBankTransactions(IEnumerable bankTransactions) + { + foreach (var bankTransaction in bankTransactions) { - foreach (var bankTransaction in bankTransactions) - { - var dbBankTransaction = BankTransaction.First(i => i.TransactionId == bankTransaction.TransactionId); - Entry(dbBankTransaction).CurrentValues.SetValues(bankTransaction); - } - return SaveChanges(); + var dbBankTransaction = BankTransaction.First(i => i.TransactionId == bankTransaction.TransactionId); + Entry(dbBankTransaction).CurrentValues.SetValues(bankTransaction); } + return SaveChanges(); + } - public int UpdateBucket(Bucket bucket) - => UpdateBuckets(new List() { bucket }); + public int UpdateBucket(Bucket bucket) + => UpdateBuckets(new List() { bucket }); - public int UpdateBuckets(IEnumerable buckets) + public int UpdateBuckets(IEnumerable buckets) + { + foreach (var bucket in buckets) { - foreach (var bucket in buckets) - { - var dbBucket = Bucket.First(i => i.BucketId == bucket.BucketId); - Entry(dbBucket).CurrentValues.SetValues(bucket); - } - return SaveChanges(); + var dbBucket = Bucket.First(i => i.BucketId == bucket.BucketId); + Entry(dbBucket).CurrentValues.SetValues(bucket); } + return SaveChanges(); + } - public int UpdateBucketGroup(BucketGroup bucketGroup) - => UpdateBucketGroups(new List() { bucketGroup }); + public int UpdateBucketGroup(BucketGroup bucketGroup) + => UpdateBucketGroups(new List() { bucketGroup }); - public int UpdateBucketGroups(IEnumerable bucketGroups) + public int UpdateBucketGroups(IEnumerable bucketGroups) + { + foreach (var bucketGroup in bucketGroups) { - foreach (var bucketGroup in bucketGroups) - { - var dbBucketGroup = BucketGroup.First(i => i.BucketGroupId == bucketGroup.BucketGroupId); - Entry(dbBucketGroup).CurrentValues.SetValues(bucketGroup); - } - return SaveChanges(); + var dbBucketGroup = BucketGroup.First(i => i.BucketGroupId == bucketGroup.BucketGroupId); + Entry(dbBucketGroup).CurrentValues.SetValues(bucketGroup); } + return SaveChanges(); + } - public int UpdateBucketMovement(BucketMovement bucketMovement) - => UpdateBucketMovements(new List() { bucketMovement }); + public int UpdateBucketMovement(BucketMovement bucketMovement) + => UpdateBucketMovements(new List() { bucketMovement }); - public int UpdateBucketMovements(IEnumerable bucketMovements) + public int UpdateBucketMovements(IEnumerable bucketMovements) + { + foreach (var bucketMovement in bucketMovements) { - foreach (var bucketMovement in bucketMovements) - { - var dbBucketMovement = BucketMovement.First(i => i.BucketMovementId == bucketMovement.BucketMovementId); - Entry(dbBucketMovement).CurrentValues.SetValues(bucketMovement); - } - return SaveChanges(); + var dbBucketMovement = BucketMovement.First(i => i.BucketMovementId == bucketMovement.BucketMovementId); + Entry(dbBucketMovement).CurrentValues.SetValues(bucketMovement); } + return SaveChanges(); + } - public int UpdateBucketVersion(BucketVersion bucketVersion) - => UpdateBucketVersions(new List() { bucketVersion }); + public int UpdateBucketVersion(BucketVersion bucketVersion) + => UpdateBucketVersions(new List() { bucketVersion }); - public int UpdateBucketVersions(IEnumerable bucketVersions) + public int UpdateBucketVersions(IEnumerable bucketVersions) + { + foreach (var bucketVersion in bucketVersions) { - foreach (var bucketVersion in bucketVersions) - { - var dbBucketVersion = BucketVersion.First(i => i.BucketVersionId == bucketVersion.BucketVersionId); - Entry(dbBucketVersion).CurrentValues.SetValues(bucketVersion); - } - return SaveChanges(); + var dbBucketVersion = BucketVersion.First(i => i.BucketVersionId == bucketVersion.BucketVersionId); + Entry(dbBucketVersion).CurrentValues.SetValues(bucketVersion); } + return SaveChanges(); + } - public int UpdateBudgetedTransaction(BudgetedTransaction budgetedTransaction) - => UpdateBudgetedTransactions(new List() { budgetedTransaction }); + public int UpdateBudgetedTransaction(BudgetedTransaction budgetedTransaction) + => UpdateBudgetedTransactions(new List() { budgetedTransaction }); - public int UpdateBudgetedTransactions(IEnumerable budgetedTransactions) + public int UpdateBudgetedTransactions(IEnumerable budgetedTransactions) + { + foreach (var budgetedTransaction in budgetedTransactions) { - foreach (var budgetedTransaction in budgetedTransactions) - { - var dbBudgetedTransaction = BudgetedTransaction.First(i => i.BudgetedTransactionId == budgetedTransaction.BudgetedTransactionId); - Entry(dbBudgetedTransaction).CurrentValues.SetValues(budgetedTransaction); - } - return SaveChanges(); + var dbBudgetedTransaction = BudgetedTransaction.First(i => i.BudgetedTransactionId == budgetedTransaction.BudgetedTransactionId); + Entry(dbBudgetedTransaction).CurrentValues.SetValues(budgetedTransaction); } + return SaveChanges(); + } - public int UpdateImportProfile(ImportProfile importProfile) - => UpdateImportProfiles(new List() { importProfile }); + public int UpdateImportProfile(ImportProfile importProfile) + => UpdateImportProfiles(new List() { importProfile }); - public int UpdateImportProfiles(IEnumerable importProfiles) + public int UpdateImportProfiles(IEnumerable importProfiles) + { + foreach (var importProfile in importProfiles) { - foreach (var importProfile in importProfiles) - { - var dbImportProfile = ImportProfile.First(i => i.ImportProfileId == importProfile.ImportProfileId); - Entry(dbImportProfile).CurrentValues.SetValues(importProfile); - } - return SaveChanges(); + var dbImportProfile = ImportProfile.First(i => i.ImportProfileId == importProfile.ImportProfileId); + Entry(dbImportProfile).CurrentValues.SetValues(importProfile); } + return SaveChanges(); + } - public int UpdateBucketRuleSet(BucketRuleSet bucketRuleSet) - => UpdateBucketRuleSets(new List() { bucketRuleSet }); + public int UpdateBucketRuleSet(BucketRuleSet bucketRuleSet) + => UpdateBucketRuleSets(new List() { bucketRuleSet }); - public int UpdateBucketRuleSets(IEnumerable bucketRuleSets) + public int UpdateBucketRuleSets(IEnumerable bucketRuleSets) + { + foreach (var bucketRuleSet in bucketRuleSets) { - foreach (var bucketRuleSet in bucketRuleSets) - { - var dbBucketRuleSet = BucketRuleSet.First(i => i.BucketRuleSetId == bucketRuleSet.BucketRuleSetId); - Entry(dbBucketRuleSet).CurrentValues.SetValues(bucketRuleSet); - } - return SaveChanges(); + var dbBucketRuleSet = BucketRuleSet.First(i => i.BucketRuleSetId == bucketRuleSet.BucketRuleSetId); + Entry(dbBucketRuleSet).CurrentValues.SetValues(bucketRuleSet); } + return SaveChanges(); + } - public int UpdateMappingRule(MappingRule mappingRule) - => UpdateMappingRules(new List() { mappingRule }); + public int UpdateMappingRule(MappingRule mappingRule) + => UpdateMappingRules(new List() { mappingRule }); - public int UpdateMappingRules(IEnumerable mappingRules) + public int UpdateMappingRules(IEnumerable mappingRules) + { + foreach (var mappingRule in mappingRules) { - foreach (var mappingRule in mappingRules) - { - var dbMappingRule = MappingRule.First(i => i.MappingRuleId == mappingRule.MappingRuleId); - Entry(dbMappingRule).CurrentValues.SetValues(mappingRule); - } - return SaveChanges(); + var dbMappingRule = MappingRule.First(i => i.MappingRuleId == mappingRule.MappingRuleId); + Entry(dbMappingRule).CurrentValues.SetValues(mappingRule); } + return SaveChanges(); + } - #endregion + #endregion - #region Delete + #region Delete - public int DeleteAccount(Account account) - => DeleteAccounts(new List() { account }); + public int DeleteAccount(Account account) + => DeleteAccounts(new List() { account }); - public int DeleteAccounts(IEnumerable accounts) - { - Account.RemoveRange(accounts); - return SaveChanges(); - } + public int DeleteAccounts(IEnumerable accounts) + { + Account.RemoveRange(accounts); + return SaveChanges(); + } - public int DeleteBankTransaction(BankTransaction bankTransaction) - => DeleteBankTransactions(new List() { bankTransaction }); + public int DeleteBankTransaction(BankTransaction bankTransaction) + => DeleteBankTransactions(new List() { bankTransaction }); - public int DeleteBankTransactions(IEnumerable bankTransactions) - { - BankTransaction.RemoveRange(bankTransactions); - return SaveChanges(); - } + public int DeleteBankTransactions(IEnumerable bankTransactions) + { + BankTransaction.RemoveRange(bankTransactions); + return SaveChanges(); + } - public int DeleteBucket(Bucket bucket) - => DeleteBuckets(new List() { bucket }); + public int DeleteBucket(Bucket bucket) + => DeleteBuckets(new List() { bucket }); - public int DeleteBuckets(IEnumerable buckets) - { - Bucket.RemoveRange(buckets); - return SaveChanges(); - } + public int DeleteBuckets(IEnumerable buckets) + { + Bucket.RemoveRange(buckets); + return SaveChanges(); + } - public int DeleteBucketGroup(BucketGroup bucketGroup) - => DeleteBucketGroups(new List() { bucketGroup }); + public int DeleteBucketGroup(BucketGroup bucketGroup) + => DeleteBucketGroups(new List() { bucketGroup }); - public int DeleteBucketGroups(IEnumerable bucketGroups) - { - BucketGroup.RemoveRange(bucketGroups); - return SaveChanges(); - } + public int DeleteBucketGroups(IEnumerable bucketGroups) + { + BucketGroup.RemoveRange(bucketGroups); + return SaveChanges(); + } - public int DeleteBucketMovement(BucketMovement bucketMovement) - => DeleteBucketMovements(new List() { bucketMovement }); + public int DeleteBucketMovement(BucketMovement bucketMovement) + => DeleteBucketMovements(new List() { bucketMovement }); - public int DeleteBucketMovements(IEnumerable bucketMovements) - { - BucketMovement.RemoveRange(bucketMovements); - return SaveChanges(); - } + public int DeleteBucketMovements(IEnumerable bucketMovements) + { + BucketMovement.RemoveRange(bucketMovements); + return SaveChanges(); + } - public int DeleteBucketVersion(BucketVersion bucketVersion) - => DeleteBucketVersions(new List() { bucketVersion }); + public int DeleteBucketVersion(BucketVersion bucketVersion) + => DeleteBucketVersions(new List() { bucketVersion }); - public int DeleteBucketVersions(IEnumerable bucketVersions) - { - BucketVersion.RemoveRange(bucketVersions); - return SaveChanges(); - } + public int DeleteBucketVersions(IEnumerable bucketVersions) + { + BucketVersion.RemoveRange(bucketVersions); + return SaveChanges(); + } - public int DeleteBudgetedTransaction(BudgetedTransaction budgetedTransaction) - => DeleteBudgetedTransactions(new List() { budgetedTransaction }); + public int DeleteBudgetedTransaction(BudgetedTransaction budgetedTransaction) + => DeleteBudgetedTransactions(new List() { budgetedTransaction }); - public int DeleteBudgetedTransactions(IEnumerable budgetedTransactions) - { - BudgetedTransaction.RemoveRange(budgetedTransactions); - return SaveChanges(); - } + public int DeleteBudgetedTransactions(IEnumerable budgetedTransactions) + { + BudgetedTransaction.RemoveRange(budgetedTransactions); + return SaveChanges(); + } - public int DeleteImportProfile(ImportProfile importProfile) - => DeleteImportProfiles(new List() { importProfile }); + public int DeleteImportProfile(ImportProfile importProfile) + => DeleteImportProfiles(new List() { importProfile }); - public int DeleteImportProfiles(IEnumerable importProfiles) - { - ImportProfile.RemoveRange(importProfiles); - return SaveChanges(); - } - - public int DeleteBucketRuleSet(BucketRuleSet bucketRuleSet) - => DeleteBucketRuleSets(new List() { bucketRuleSet }); + public int DeleteImportProfiles(IEnumerable importProfiles) + { + ImportProfile.RemoveRange(importProfiles); + return SaveChanges(); + } - public int DeleteBucketRuleSets(IEnumerable bucketRuleSets) - { - BucketRuleSet.RemoveRange(bucketRuleSets); - return SaveChanges(); - } + public int DeleteBucketRuleSet(BucketRuleSet bucketRuleSet) + => DeleteBucketRuleSets(new List() { bucketRuleSet }); - public int DeleteMappingRule(MappingRule mappingRule) - => DeleteMappingRules(new List() { mappingRule }); + public int DeleteBucketRuleSets(IEnumerable bucketRuleSets) + { + BucketRuleSet.RemoveRange(bucketRuleSets); + return SaveChanges(); + } - public int DeleteMappingRules(IEnumerable mappingRules) - { - MappingRule.RemoveRange(mappingRules); - return SaveChanges(); - } + public int DeleteMappingRule(MappingRule mappingRule) + => DeleteMappingRules(new List() { mappingRule }); - #endregion + public int DeleteMappingRules(IEnumerable mappingRules) + { + MappingRule.RemoveRange(mappingRules); + return SaveChanges(); } + + #endregion } + diff --git a/OpenBudgeteer.Core/Common/Database/MySqlDatabaseContext.cs b/OpenBudgeteer.Core/Common/Database/MySqlDatabaseContext.cs index 9edbe91..7a6d795 100644 --- a/OpenBudgeteer.Core/Common/Database/MySqlDatabaseContext.cs +++ b/OpenBudgeteer.Core/Common/Database/MySqlDatabaseContext.cs @@ -1,20 +1,19 @@ using Microsoft.EntityFrameworkCore; -namespace OpenBudgeteer.Core.Common.Database +namespace OpenBudgeteer.Core.Common.Database; + +public class MySqlDatabaseContext : DatabaseContext { - public class MySqlDatabaseContext : DatabaseContext + private const string CharacterSet = "utf8mb4"; + + public MySqlDatabaseContext(DbContextOptions options) : base(options) { - private const string CharacterSet = "utf8mb4"; - - public MySqlDatabaseContext(DbContextOptions options) : base(options) - { - } + } - protected override void OnModelCreating(ModelBuilder modelBuilder) - { - modelBuilder.HasCharSet(CharacterSet); - - base.OnModelCreating(modelBuilder); - } + protected override void OnModelCreating(ModelBuilder modelBuilder) + { + modelBuilder.HasCharSet(CharacterSet); + + base.OnModelCreating(modelBuilder); } } diff --git a/OpenBudgeteer.Core/Common/Database/MySqlDatabaseContextFactory.cs b/OpenBudgeteer.Core/Common/Database/MySqlDatabaseContextFactory.cs index 20bdc07..024da03 100644 --- a/OpenBudgeteer.Core/Common/Database/MySqlDatabaseContextFactory.cs +++ b/OpenBudgeteer.Core/Common/Database/MySqlDatabaseContextFactory.cs @@ -2,51 +2,51 @@ using Microsoft.EntityFrameworkCore.Design; using Microsoft.Extensions.Configuration; -namespace OpenBudgeteer.Core.Common.Database +namespace OpenBudgeteer.Core.Common.Database; + +public class MySqlDatabaseContextFactory : IDesignTimeDbContextFactory { - public class MySqlDatabaseContextFactory : IDesignTimeDbContextFactory + private const string MysqlConnectionString = "Server=192.168.178.30;" + + "Port=3306;" + + "Database=openbudgeteer-dev;" + + "User=openbudgeteer-dev;" + + "Password=openbudgeteer-dev"; + /* + * Pass arguments to args by appending them to the tool call. + * i.e.: dotnet ef migrations add Test -c OpenBudgeteer.Core.Common.Database.MySqlDatabaseContext -- "Server=kitana.vdaa;Port=3307;Database=openbudgeteer-dev;User=openbudgeteer-dev;Password=openbudgeteer-dev" + */ + public MySqlDatabaseContext CreateDbContext(string[] args) { - private const string MysqlConnectionString = "Server=192.168.178.30;" + - "Port=3306;" + - "Database=openbudgeteer-dev;" + - "User=openbudgeteer-dev;" + - "Password=openbudgeteer-dev"; - /* - * Pass arguments to args by appending them to the tool call. - * i.e.: dotnet ef migrations add Test -c OpenBudgeteer.Core.Common.Database.MySqlDatabaseContext -- "Server=kitana.vdaa;Port=3307;Database=openbudgeteer-dev;User=openbudgeteer-dev;Password=openbudgeteer-dev" - */ - public MySqlDatabaseContext CreateDbContext(string[] args) - { - var optionsBuilder = new DbContextOptionsBuilder(); - - if ((args?.Length ?? 0) == 0) - { - optionsBuilder.UseMySql(MysqlConnectionString, ServerVersion.AutoDetect(MysqlConnectionString)); - } - else - { - var connectionString = string.Join(";", args); - optionsBuilder.UseMySql(connectionString, ServerVersion.AutoDetect(connectionString)); - } + var optionsBuilder = new DbContextOptionsBuilder(); - return new MySqlDatabaseContext(optionsBuilder.Options); + if ((args?.Length ?? 0) == 0) + { + optionsBuilder.UseMySql(MysqlConnectionString, ServerVersion.AutoDetect(MysqlConnectionString)); } - - public MySqlDatabaseContext CreateDbContext(IConfiguration configuration) + else { - var configurationSection = configuration.GetSection("Connection"); - var connectionString = $"Server={configurationSection?["Server"]};" + - $"Port={configurationSection?["Port"]};" + - $"Database={configurationSection?["Database"]};" + - $"User={configurationSection?["User"]};" + - $"Password={configurationSection?["Password"]}"; - var optionsBuilder = new DbContextOptionsBuilder() - .UseMySql( - connectionString, - ServerVersion.AutoDetect(connectionString), - b => b.MigrationsAssembly("OpenBudgeteer.Core")); - - return new MySqlDatabaseContext(optionsBuilder.Options); + var connectionString = string.Join(";", args); + optionsBuilder.UseMySql(connectionString, ServerVersion.AutoDetect(connectionString)); } + + return new MySqlDatabaseContext(optionsBuilder.Options); + } + + public MySqlDatabaseContext CreateDbContext(IConfiguration configuration) + { + var configurationSection = configuration.GetSection("Connection"); + var connectionString = $"Server={configurationSection?["Server"]};" + + $"Port={configurationSection?["Port"]};" + + $"Database={configurationSection?["Database"]};" + + $"User={configurationSection?["User"]};" + + $"Password={configurationSection?["Password"]}"; + var optionsBuilder = new DbContextOptionsBuilder() + .UseMySql( + connectionString, + ServerVersion.AutoDetect(connectionString), + b => b.MigrationsAssembly("OpenBudgeteer.Core")); + + return new MySqlDatabaseContext(optionsBuilder.Options); } } + diff --git a/OpenBudgeteer.Core/Common/Database/SqliteDatabaseContext.cs b/OpenBudgeteer.Core/Common/Database/SqliteDatabaseContext.cs index 29df075..497877b 100644 --- a/OpenBudgeteer.Core/Common/Database/SqliteDatabaseContext.cs +++ b/OpenBudgeteer.Core/Common/Database/SqliteDatabaseContext.cs @@ -1,15 +1,12 @@ -using System; -using System.Collections.Generic; -using System.Text; -using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore; -namespace OpenBudgeteer.Core.Common.Database +namespace OpenBudgeteer.Core.Common.Database; + +public class SqliteDatabaseContext : DatabaseContext { - public class SqliteDatabaseContext : DatabaseContext + public SqliteDatabaseContext(DbContextOptions options) : base(options) { - public SqliteDatabaseContext(DbContextOptions options) : base(options) - { - - } + } } + diff --git a/OpenBudgeteer.Core/Common/Database/SqliteDatabaseContextFactory.cs b/OpenBudgeteer.Core/Common/Database/SqliteDatabaseContextFactory.cs index a4da3dc..8fd8864 100644 --- a/OpenBudgeteer.Core/Common/Database/SqliteDatabaseContextFactory.cs +++ b/OpenBudgeteer.Core/Common/Database/SqliteDatabaseContextFactory.cs @@ -1,29 +1,24 @@ -using System; -using System.Collections.Generic; -using System.Text; -using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore.Design; -using Microsoft.Extensions.Configuration; -namespace OpenBudgeteer.Core.Common.Database +namespace OpenBudgeteer.Core.Common.Database; + +public class SqliteDatabaseContextFactory : IDesignTimeDbContextFactory { - public class SqliteDatabaseContextFactory : IDesignTimeDbContextFactory + public SqliteDatabaseContext CreateDbContext(string[] args) { - public SqliteDatabaseContext CreateDbContext(string[] args) - { - var optionsBuilder = new DbContextOptionsBuilder(); - optionsBuilder.UseSqlite("Data Source=openbudgeteer.db"); + var optionsBuilder = new DbContextOptionsBuilder(); + optionsBuilder.UseSqlite("Data Source=openbudgeteer.db"); - return new SqliteDatabaseContext(optionsBuilder.Options); - } + return new SqliteDatabaseContext(optionsBuilder.Options); + } - public SqliteDatabaseContext CreateDbContext(string connectionString) - { - var optionsBuilder = new DbContextOptionsBuilder() - .UseSqlite( - connectionString, - b => b.MigrationsAssembly("OpenBudgeteer.Core")); - return new SqliteDatabaseContext(optionsBuilder.Options); - } + public SqliteDatabaseContext CreateDbContext(string connectionString) + { + var optionsBuilder = new DbContextOptionsBuilder() + .UseSqlite( + connectionString, + b => b.MigrationsAssembly("OpenBudgeteer.Core")); + return new SqliteDatabaseContext(optionsBuilder.Options); } } diff --git a/OpenBudgeteer.Core/Common/EventClasses/AmountChangedArgs.cs b/OpenBudgeteer.Core/Common/EventClasses/AmountChangedArgs.cs index fbc497f..133b8a9 100644 --- a/OpenBudgeteer.Core/Common/EventClasses/AmountChangedArgs.cs +++ b/OpenBudgeteer.Core/Common/EventClasses/AmountChangedArgs.cs @@ -1,24 +1,20 @@ -using OpenBudgeteer.Core.ViewModels; -using OpenBudgeteer.Core.ViewModels.ItemViewModels; +using OpenBudgeteer.Core.ViewModels.ItemViewModels; using System; -using System.Collections.Generic; -using System.Text; -namespace OpenBudgeteer.Core.Common.EventClasses +namespace OpenBudgeteer.Core.Common.EventClasses; + +/// +/// Event Handler Argument for setting the amount in +/// +public class AmountChangedArgs : EventArgs { - /// - /// Event Handler Argument for setting the amount in - /// - public class AmountChangedArgs : EventArgs - { - public PartialBucketViewModelItem Source { get; private set; } + public PartialBucketViewModelItem Source { get; private set; } - public decimal NewAmount { get; private set; } + public decimal NewAmount { get; private set; } - public AmountChangedArgs(PartialBucketViewModelItem source, decimal newAmount) - { - Source = source; - NewAmount = newAmount; - } + public AmountChangedArgs(PartialBucketViewModelItem source, decimal newAmount) + { + Source = source; + NewAmount = newAmount; } } diff --git a/OpenBudgeteer.Core/Common/EventClasses/DeleteAssignmentRequestArgs.cs b/OpenBudgeteer.Core/Common/EventClasses/DeleteAssignmentRequestArgs.cs index 20d251c..21fb37b 100644 --- a/OpenBudgeteer.Core/Common/EventClasses/DeleteAssignmentRequestArgs.cs +++ b/OpenBudgeteer.Core/Common/EventClasses/DeleteAssignmentRequestArgs.cs @@ -1,21 +1,17 @@ -using OpenBudgeteer.Core.ViewModels; -using OpenBudgeteer.Core.ViewModels.ItemViewModels; +using OpenBudgeteer.Core.ViewModels.ItemViewModels; using System; -using System.Collections.Generic; -using System.Text; -namespace OpenBudgeteer.Core.Common.EventClasses +namespace OpenBudgeteer.Core.Common.EventClasses; + +/// +/// Event Handler Argument for requesting the deletion of a bucket assignment +/// +public class DeleteAssignmentRequestArgs : EventArgs { - /// - /// Event Handler Argument for requesting the deletion of a bucket assignment - /// - public class DeleteAssignmentRequestArgs : EventArgs - { - public PartialBucketViewModelItem Source { get; private set; } + public PartialBucketViewModelItem Source { get; private set; } - public DeleteAssignmentRequestArgs(PartialBucketViewModelItem source) - { - Source = source; - } + public DeleteAssignmentRequestArgs(PartialBucketViewModelItem source) + { + Source = source; } } diff --git a/OpenBudgeteer.Core/Common/EventClasses/ProposeBucketChangedEventArgs.cs b/OpenBudgeteer.Core/Common/EventClasses/ProposeBucketChangedEventArgs.cs index cfa4265..6c0c7fd 100644 --- a/OpenBudgeteer.Core/Common/EventClasses/ProposeBucketChangedEventArgs.cs +++ b/OpenBudgeteer.Core/Common/EventClasses/ProposeBucketChangedEventArgs.cs @@ -1,20 +1,15 @@ using System; -using System.Collections.Generic; -using System.Linq; -using System.Text; -using System.Threading.Tasks; -namespace OpenBudgeteer.Core.Common.EventClasses +namespace OpenBudgeteer.Core.Common.EventClasses; + +public class ProposeBucketChangedEventArgs : EventArgs { - public class ProposeBucketChangedEventArgs : EventArgs - { - public int NewValue { get; private set; } - public int NewProgress { get; private set; } + public int NewValue { get; private set; } + public int NewProgress { get; private set; } - public ProposeBucketChangedEventArgs(int newValue, int newProgress) - { - NewValue = newValue; - NewProgress = newProgress; - } + public ProposeBucketChangedEventArgs(int newValue, int newProgress) + { + NewValue = newValue; + NewProgress = newProgress; } } diff --git a/OpenBudgeteer.Core/Common/EventClasses/ViewModelReloadEventArgs.cs b/OpenBudgeteer.Core/Common/EventClasses/ViewModelReloadEventArgs.cs index 6bea86b..5343274 100644 --- a/OpenBudgeteer.Core/Common/EventClasses/ViewModelReloadEventArgs.cs +++ b/OpenBudgeteer.Core/Common/EventClasses/ViewModelReloadEventArgs.cs @@ -1,17 +1,14 @@ using System; -using System.Collections.Generic; -using System.Text; using OpenBudgeteer.Core.ViewModels; -namespace OpenBudgeteer.Core.Common.EventClasses +namespace OpenBudgeteer.Core.Common.EventClasses; + +public class ViewModelReloadEventArgs : EventArgs { - public class ViewModelReloadEventArgs : EventArgs - { - public ViewModelBase ViewModel { get; private set; } + public ViewModelBase ViewModel { get; private set; } - public ViewModelReloadEventArgs(ViewModelBase viewModel) - { - ViewModel = viewModel; - } + public ViewModelReloadEventArgs(ViewModelBase viewModel) + { + ViewModel = viewModel; } } diff --git a/OpenBudgeteer.Core/Common/MonthOutputConverter.cs b/OpenBudgeteer.Core/Common/MonthOutputConverter.cs index 69fef1e..637241f 100644 --- a/OpenBudgeteer.Core/Common/MonthOutputConverter.cs +++ b/OpenBudgeteer.Core/Common/MonthOutputConverter.cs @@ -1,31 +1,30 @@ using System; using System.Globalization; -namespace OpenBudgeteer.Core.Common +namespace OpenBudgeteer.Core.Common; + +public class MonthOutputConverter { - public class MonthOutputConverter + public object Convert(object value, Type targetType, object parameter, CultureInfo culture) { - public object Convert(object value, Type targetType, object parameter, CultureInfo culture) - { - if (!(value is int month)) return string.Empty; - var date = new DateTime(1, month, 1); - return date.ToString("MMM", culture); - } + if (!(value is int month)) return string.Empty; + var date = new DateTime(1, month, 1); + return date.ToString("MMM", culture); + } - public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture) + public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture) + { + if ((!(value is string month))) return DateTime.Now.Month; + for (var i = 1; i < 13; i++) { - if ((!(value is string month))) return DateTime.Now.Month; - for (var i = 1; i < 13; i++) - { - var date = new DateTime(1, i, 1); - if (date.ToString("MMM") == month) return i; - } - return DateTime.Now.Month; + var date = new DateTime(1, i, 1); + if (date.ToString("MMM") == month) return i; } + return DateTime.Now.Month; + } - public string ConvertMonth(object value, CultureInfo culture = null) - { - return Convert(value, typeof(string), null, culture ?? CultureInfo.CurrentCulture).ToString(); - } + public string ConvertMonth(object value, CultureInfo culture = null) + { + return Convert(value, typeof(string), null, culture ?? CultureInfo.CurrentCulture).ToString(); } } diff --git a/OpenBudgeteer.Core/Common/ViewModelOperationResult.cs b/OpenBudgeteer.Core/Common/ViewModelOperationResult.cs index af08105..4a49792 100644 --- a/OpenBudgeteer.Core/Common/ViewModelOperationResult.cs +++ b/OpenBudgeteer.Core/Common/ViewModelOperationResult.cs @@ -1,29 +1,24 @@ -using System; -using System.Collections.Generic; -using System.Text; +namespace OpenBudgeteer.Core.Common; -namespace OpenBudgeteer.Core.Common +public class ViewModelOperationResult { - public class ViewModelOperationResult - { - public bool IsSuccessful { get; } - public string Message { get; } - public bool ViewModelReloadRequired { get; } + public bool IsSuccessful { get; } + public string Message { get; } + public bool ViewModelReloadRequired { get; } - public ViewModelOperationResult(bool isSuccessful, string message, bool viewModelReloadRequired = false) - { - IsSuccessful = isSuccessful; - Message = message; - ViewModelReloadRequired = viewModelReloadRequired; - } + public ViewModelOperationResult(bool isSuccessful, string message, bool viewModelReloadRequired = false) + { + IsSuccessful = isSuccessful; + Message = message; + ViewModelReloadRequired = viewModelReloadRequired; + } - public ViewModelOperationResult(bool isSuccessful, bool viewModelReloadInvoked = false) - : this(isSuccessful, string.Empty, viewModelReloadInvoked) + public ViewModelOperationResult(bool isSuccessful, bool viewModelReloadInvoked = false) + : this(isSuccessful, string.Empty, viewModelReloadInvoked) + { + if (!isSuccessful) { - if (!isSuccessful) - { - Message = "Unknown Error."; - } + Message = "Unknown Error."; } } } diff --git a/OpenBudgeteer.Core/Models/Account.cs b/OpenBudgeteer.Core/Models/Account.cs index b06f053..c884451 100644 --- a/OpenBudgeteer.Core/Models/Account.cs +++ b/OpenBudgeteer.Core/Models/Account.cs @@ -1,30 +1,25 @@ -using System; -using System.Collections.Generic; -using System.Text; +namespace OpenBudgeteer.Core.Models; -namespace OpenBudgeteer.Core.Models +public class Account : BaseObject { - public class Account : BaseObject + private int _accountId; + public int AccountId { - private int _accountId; - public int AccountId - { - get => _accountId; - set => Set(ref _accountId, value); - } + get => _accountId; + set => Set(ref _accountId, value); + } - private string _name; - public string Name - { - get => _name; - set => Set(ref _name, value); - } + private string _name; + public string Name + { + get => _name; + set => Set(ref _name, value); + } - private int _isActive; - public int IsActive - { - get => _isActive; - set => Set(ref _isActive, value); - } + private int _isActive; + public int IsActive + { + get => _isActive; + set => Set(ref _isActive, value); } } diff --git a/OpenBudgeteer.Core/Models/BankTransaction.cs b/OpenBudgeteer.Core/Models/BankTransaction.cs index 3f0c635..c8ee871 100644 --- a/OpenBudgeteer.Core/Models/BankTransaction.cs +++ b/OpenBudgeteer.Core/Models/BankTransaction.cs @@ -1,57 +1,53 @@ using System; -using System.Collections.Generic; -using System.Text; -using System.Text.RegularExpressions; using System.ComponentModel.DataAnnotations; using System.ComponentModel.DataAnnotations.Schema; -namespace OpenBudgeteer.Core.Models +namespace OpenBudgeteer.Core.Models; + +public class BankTransaction : BaseObject { - public class BankTransaction : BaseObject + private int _transactionId; + [Key] + public int TransactionId { - private int _transactionId; - [Key] - public int TransactionId - { - get => _transactionId; - set => Set(ref _transactionId, value); - } + get => _transactionId; + set => Set(ref _transactionId, value); + } - private int _accountId; - [Required] - public int AccountId - { - get => _accountId; - set => Set(ref _accountId, value); - } + private int _accountId; + [Required] + public int AccountId + { + get => _accountId; + set => Set(ref _accountId, value); + } - private DateTime _transactionDate; - public DateTime TransactionDate - { - get => _transactionDate; - set => Set(ref _transactionDate, value); - } + private DateTime _transactionDate; + public DateTime TransactionDate + { + get => _transactionDate; + set => Set(ref _transactionDate, value); + } - private string _payee; - public string Payee - { - get => _payee; - set => Set(ref _payee, value); - } + private string _payee; + public string Payee + { + get => _payee; + set => Set(ref _payee, value); + } - private string _memo; - public string Memo - { - get => _memo; - set => Set(ref _memo, value); - } + private string _memo; + public string Memo + { + get => _memo; + set => Set(ref _memo, value); + } - private decimal _amount; - [Column(TypeName = "decimal(65, 2)")] - public decimal Amount - { - get => _amount; - set => Set(ref _amount, value); - } + private decimal _amount; + [Column(TypeName = "decimal(65, 2)")] + public decimal Amount + { + get => _amount; + set => Set(ref _amount, value); } } diff --git a/OpenBudgeteer.Core/Models/BaseObject.cs b/OpenBudgeteer.Core/Models/BaseObject.cs index d625958..2872b1d 100644 --- a/OpenBudgeteer.Core/Models/BaseObject.cs +++ b/OpenBudgeteer.Core/Models/BaseObject.cs @@ -1,29 +1,26 @@ -using System; -using System.Collections.Generic; +using System.Collections.Generic; using System.ComponentModel; using System.Runtime.CompilerServices; -using System.Text; -namespace OpenBudgeteer.Core.Models +namespace OpenBudgeteer.Core.Models; + +public class BaseObject : INotifyPropertyChanged { - public class BaseObject : INotifyPropertyChanged + protected bool Set(ref T field, T value, [CallerMemberName] string propertyName = "") { - protected bool Set(ref T field, T value, [CallerMemberName] string propertyName = "") + if (EqualityComparer.Default.Equals(field, value)) { - if (EqualityComparer.Default.Equals(field, value)) - { - return false; - } - field = value; - NotifyPropertyChanged(propertyName); - return true; + return false; } + field = value; + NotifyPropertyChanged(propertyName); + return true; + } - public event PropertyChangedEventHandler PropertyChanged; + public event PropertyChangedEventHandler PropertyChanged; - protected void NotifyPropertyChanged(string propertyName) - { - PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName)); - } + protected void NotifyPropertyChanged(string propertyName) + { + PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName)); } } diff --git a/OpenBudgeteer.Core/Models/Bucket.cs b/OpenBudgeteer.Core/Models/Bucket.cs index a24d7f9..9fce1f9 100644 --- a/OpenBudgeteer.Core/Models/Bucket.cs +++ b/OpenBudgeteer.Core/Models/Bucket.cs @@ -1,68 +1,63 @@ using System; -using System.Collections.Generic; -using System.Text; -using System.Text.RegularExpressions; using System.ComponentModel.DataAnnotations; using System.ComponentModel.DataAnnotations.Schema; -using System.Diagnostics; using System.Drawing; -namespace OpenBudgeteer.Core.Models +namespace OpenBudgeteer.Core.Models; + +public class Bucket : BaseObject { - public class Bucket : BaseObject + private int _bucketId; + public int BucketId { - private int _bucketId; - public int BucketId - { - get => _bucketId; - set => Set(ref _bucketId, value); - } + get => _bucketId; + set => Set(ref _bucketId, value); + } - private string _name; - public string Name - { - get => _name; - set => Set(ref _name, value); - } + private string _name; + public string Name + { + get => _name; + set => Set(ref _name, value); + } - private int _bucketGroupId; - [Required] - public int BucketGroupId - { - get => _bucketGroupId; - set => Set(ref _bucketGroupId, value); - } + private int _bucketGroupId; + [Required] + public int BucketGroupId + { + get => _bucketGroupId; + set => Set(ref _bucketGroupId, value); + } - private string _colorCode; - public string ColorCode - { - get => _colorCode; - set => Set(ref _colorCode, value); - } + private string _colorCode; + public string ColorCode + { + get => _colorCode; + set => Set(ref _colorCode, value); + } - [NotMapped] - public Color Color => string.IsNullOrEmpty(ColorCode) ? Color.LightGray : Color.FromName(ColorCode); + [NotMapped] + public Color Color => string.IsNullOrEmpty(ColorCode) ? Color.LightGray : Color.FromName(ColorCode); - private DateTime _validFrom; - [Required] - public DateTime ValidFrom - { - get => _validFrom; - set => Set(ref _validFrom, value); - } + private DateTime _validFrom; + [Required] + public DateTime ValidFrom + { + get => _validFrom; + set => Set(ref _validFrom, value); + } - private bool _isInactive; - public bool IsInactive - { - get => _isInactive; - set => Set(ref _isInactive, value); - } + private bool _isInactive; + public bool IsInactive + { + get => _isInactive; + set => Set(ref _isInactive, value); + } - private DateTime _isInactiveFrom; - public DateTime IsInactiveFrom - { - get => _isInactiveFrom; - set => Set(ref _isInactiveFrom, value); - } + private DateTime _isInactiveFrom; + public DateTime IsInactiveFrom + { + get => _isInactiveFrom; + set => Set(ref _isInactiveFrom, value); } } diff --git a/OpenBudgeteer.Core/Models/BucketGroup.cs b/OpenBudgeteer.Core/Models/BucketGroup.cs index f35cae0..52a99b4 100644 --- a/OpenBudgeteer.Core/Models/BucketGroup.cs +++ b/OpenBudgeteer.Core/Models/BucketGroup.cs @@ -1,33 +1,28 @@ -using System; -using System.Collections.Generic; -using System.Text; -using System.ComponentModel.DataAnnotations; -using System.ComponentModel.DataAnnotations.Schema; +using System.ComponentModel.DataAnnotations; -namespace OpenBudgeteer.Core.Models +namespace OpenBudgeteer.Core.Models; + +public class BucketGroup : BaseObject { - public class BucketGroup : BaseObject + private int _bucketGroupId; + public int BucketGroupId { - private int _bucketGroupId; - public int BucketGroupId - { - get => _bucketGroupId; - set => Set(ref _bucketGroupId, value); - } + get => _bucketGroupId; + set => Set(ref _bucketGroupId, value); + } - private string _name; - public string Name - { - get => _name; - set => Set(ref _name, value); - } + private string _name; + public string Name + { + get => _name; + set => Set(ref _name, value); + } - private int _position; - [Required] - public int Position - { - get => _position; - set => Set(ref _position, value); - } + private int _position; + [Required] + public int Position + { + get => _position; + set => Set(ref _position, value); } } diff --git a/OpenBudgeteer.Core/Models/BucketMovement.cs b/OpenBudgeteer.Core/Models/BucketMovement.cs index 29be4df..662a49c 100644 --- a/OpenBudgeteer.Core/Models/BucketMovement.cs +++ b/OpenBudgeteer.Core/Models/BucketMovement.cs @@ -1,51 +1,47 @@ using System; -using System.Collections.Generic; -using System.Text; -using System.Text.RegularExpressions; using System.ComponentModel.DataAnnotations; using System.ComponentModel.DataAnnotations.Schema; -namespace OpenBudgeteer.Core.Models +namespace OpenBudgeteer.Core.Models; + +public class BucketMovement : BaseObject { - public class BucketMovement : BaseObject + private int _bucketMovementId; + public int BucketMovementId { - private int _bucketMovementId; - public int BucketMovementId - { - get => _bucketMovementId; - set => Set(ref _bucketMovementId, value); - } + get => _bucketMovementId; + set => Set(ref _bucketMovementId, value); + } - private int _bucketId; - [Required] - public int BucketId - { - get => _bucketId; - set => Set(ref _bucketId, value); - } + private int _bucketId; + [Required] + public int BucketId + { + get => _bucketId; + set => Set(ref _bucketId, value); + } - private decimal _amount; - [Column(TypeName = "decimal(65, 2)")] - public decimal Amount - { - get => _amount; - set => Set(ref _amount, value); - } + private decimal _amount; + [Column(TypeName = "decimal(65, 2)")] + public decimal Amount + { + get => _amount; + set => Set(ref _amount, value); + } - private DateTime _movementDate; - public DateTime MovementDate - { - get => _movementDate; - set => Set(ref _movementDate, value); - } + private DateTime _movementDate; + public DateTime MovementDate + { + get => _movementDate; + set => Set(ref _movementDate, value); + } - public BucketMovement() { } + public BucketMovement() { } - public BucketMovement(Bucket bucket, decimal amount, DateTime date) : this() - { - BucketId = bucket.BucketId; - Amount = amount; - MovementDate = date; - } + public BucketMovement(Bucket bucket, decimal amount, DateTime date) : this() + { + BucketId = bucket.BucketId; + Amount = amount; + MovementDate = date; } } diff --git a/OpenBudgeteer.Core/Models/BucketRuleSet.cs b/OpenBudgeteer.Core/Models/BucketRuleSet.cs index c3bc90e..b58a521 100644 --- a/OpenBudgeteer.Core/Models/BucketRuleSet.cs +++ b/OpenBudgeteer.Core/Models/BucketRuleSet.cs @@ -1,42 +1,36 @@ -using System; -using System.Collections.Generic; -using System.Collections.ObjectModel; -using System.ComponentModel.DataAnnotations; -using System.Diagnostics; -using System.Text; +using System.ComponentModel.DataAnnotations; -namespace OpenBudgeteer.Core.Models +namespace OpenBudgeteer.Core.Models; + +public class BucketRuleSet : BaseObject { - public class BucketRuleSet : BaseObject + private int _bucketRuleSetId; + public int BucketRuleSetId { - private int _bucketRuleSetId; - public int BucketRuleSetId - { - get => _bucketRuleSetId; - set => Set(ref _bucketRuleSetId, value); - } + get => _bucketRuleSetId; + set => Set(ref _bucketRuleSetId, value); + } - private int _priority; - [Required] - public int Priority - { - get => _priority; - set => Set(ref _priority, value); - } + private int _priority; + [Required] + public int Priority + { + get => _priority; + set => Set(ref _priority, value); + } - private string _name; - public string Name - { - get => _name; - set => Set(ref _name, value); - } + private string _name; + public string Name + { + get => _name; + set => Set(ref _name, value); + } - private int _targetBucketId; - [Required] - public int TargetBucketId - { - get => _targetBucketId; - set => Set(ref _targetBucketId, value); - } + private int _targetBucketId; + [Required] + public int TargetBucketId + { + get => _targetBucketId; + set => Set(ref _targetBucketId, value); } } diff --git a/OpenBudgeteer.Core/Models/BucketVersion.cs b/OpenBudgeteer.Core/Models/BucketVersion.cs index bf17921..d20d140 100644 --- a/OpenBudgeteer.Core/Models/BucketVersion.cs +++ b/OpenBudgeteer.Core/Models/BucketVersion.cs @@ -1,134 +1,130 @@ using System; -using System.Collections.Generic; -using System.Text; -using System.Text.RegularExpressions; using System.ComponentModel.DataAnnotations; using System.ComponentModel.DataAnnotations.Schema; -namespace OpenBudgeteer.Core.Models +namespace OpenBudgeteer.Core.Models; + +public class BucketVersion : BaseObject { - public class BucketVersion : BaseObject + private int _bucketVersionId; + public int BucketVersionId { - private int _bucketVersionId; - public int BucketVersionId - { - get => _bucketVersionId; - set => Set(ref _bucketVersionId, value); - } + get => _bucketVersionId; + set => Set(ref _bucketVersionId, value); + } - private int _bucketId; - [Required] - public int BucketId - { - get => _bucketId; - set => Set(ref _bucketId, value); - } + private int _bucketId; + [Required] + public int BucketId + { + get => _bucketId; + set => Set(ref _bucketId, value); + } - private int _version; - [Required] - public int Version - { - get => _version; - set => Set(ref _version, value); - } + private int _version; + [Required] + public int Version + { + get => _version; + set => Set(ref _version, value); + } - private int _bucketType; - /// - /// Bucket Types: - /// - /// 1 - Standard Bucket
- /// 2 - Monthly expense
- /// 3 - Expense every X Months
- /// 4 - Save X until Y date - ///
- ///
- [Required] - public int BucketType + private int _bucketType; + /// + /// Bucket Types: + /// + /// 1 - Standard Bucket
+ /// 2 - Monthly expense
+ /// 3 - Expense every X Months
+ /// 4 - Save X until Y date + ///
+ ///
+ [Required] + public int BucketType + { + get => _bucketType; + set { - get => _bucketType; - set + Set(ref _bucketType, value); + switch (value) { - Set(ref _bucketType, value); - switch (value) - { - case 1: - BucketTypeXParam = 0; - BucketTypeYParam = 0; - BucketTypeZParam = DateTime.MinValue; - break; - case 2: - BucketTypeXParam = 1; - BucketTypeZParam = DateTime.MinValue; - break; - case 3: - break; - case 4: - BucketTypeXParam = 0; - break; - } + case 1: + BucketTypeXParam = 0; + BucketTypeYParam = 0; + BucketTypeZParam = DateTime.MinValue; + break; + case 2: + BucketTypeXParam = 1; + BucketTypeZParam = DateTime.MinValue; + break; + case 3: + break; + case 4: + BucketTypeXParam = 0; + break; } } + } - private int _bucketTypeXParam; - /// - /// Parameter for number of months. For BucketType: - /// - /// 1 - 0
- /// 2 - 1
- /// 3 - int Months
- /// 4 - 0 - ///
- ///
- public int BucketTypeXParam - { - get => _bucketTypeXParam; - set => Set(ref _bucketTypeXParam, value); - } + private int _bucketTypeXParam; + /// + /// Parameter for number of months. For BucketType: + /// + /// 1 - 0
+ /// 2 - 1
+ /// 3 - int Months
+ /// 4 - 0 + ///
+ ///
+ public int BucketTypeXParam + { + get => _bucketTypeXParam; + set => Set(ref _bucketTypeXParam, value); + } - private decimal _bucketTypeYParam; - /// - /// Parameter for an Amount value. For BucketType: - /// - /// 1 - 0
- /// 2-3 - decimal Amount
- /// 4 - decimal Amount - ///
- ///
- [Column(TypeName = "decimal(65, 2)")] - public decimal BucketTypeYParam - { - get => _bucketTypeYParam; - set => Set(ref _bucketTypeYParam, value); - } + private decimal _bucketTypeYParam; + /// + /// Parameter for an Amount value. For BucketType: + /// + /// 1 - 0
+ /// 2-3 - decimal Amount
+ /// 4 - decimal Amount + ///
+ ///
+ [Column(TypeName = "decimal(65, 2)")] + public decimal BucketTypeYParam + { + get => _bucketTypeYParam; + set => Set(ref _bucketTypeYParam, value); + } - private DateTime _bucketTypeZParam; - /// - /// Parameter for target date value. For BucketType: - /// - /// 1-2 - string.Empty
- /// 3 - DateTime First target date
- /// 4 - DateTime Target date - ///
- ///
- public DateTime BucketTypeZParam - { - get => _bucketTypeZParam; - set => Set(ref _bucketTypeZParam, value); - } + private DateTime _bucketTypeZParam; + /// + /// Parameter for target date value. For BucketType: + /// + /// 1-2 - string.Empty
+ /// 3 - DateTime First target date
+ /// 4 - DateTime Target date + ///
+ ///
+ public DateTime BucketTypeZParam + { + get => _bucketTypeZParam; + set => Set(ref _bucketTypeZParam, value); + } - private string _notes; - public string Notes - { - get => _notes; - set => Set(ref _notes, value); - } + private string _notes; + public string Notes + { + get => _notes; + set => Set(ref _notes, value); + } - private DateTime _validFrom; - [Required] - public DateTime ValidFrom - { - get => _validFrom; - set => Set(ref _validFrom, value); - } + private DateTime _validFrom; + [Required] + public DateTime ValidFrom + { + get => _validFrom; + set => Set(ref _validFrom, value); } } diff --git a/OpenBudgeteer.Core/Models/BudgetedTransaction.cs b/OpenBudgeteer.Core/Models/BudgetedTransaction.cs index 3310267..dc01396 100644 --- a/OpenBudgeteer.Core/Models/BudgetedTransaction.cs +++ b/OpenBudgeteer.Core/Models/BudgetedTransaction.cs @@ -1,42 +1,38 @@ -using System; -using System.Collections.Generic; -using System.Text; -using System.ComponentModel.DataAnnotations; +using System.ComponentModel.DataAnnotations; using System.ComponentModel.DataAnnotations.Schema; -namespace OpenBudgeteer.Core.Models +namespace OpenBudgeteer.Core.Models; + +public class BudgetedTransaction : BaseObject { - public class BudgetedTransaction : BaseObject + private int _budgetedTransactionId; + public int BudgetedTransactionId { - private int _budgetedTransactionId; - public int BudgetedTransactionId - { - get => _budgetedTransactionId; - set => Set(ref _budgetedTransactionId, value); - } + get => _budgetedTransactionId; + set => Set(ref _budgetedTransactionId, value); + } - private int _transactionId; - [Required] - public int TransactionId - { - get => _transactionId; - set => Set(ref _transactionId, value); - } + private int _transactionId; + [Required] + public int TransactionId + { + get => _transactionId; + set => Set(ref _transactionId, value); + } - private int _bucketId; - [Required] - public int BucketId - { - get => _bucketId; - set => Set(ref _bucketId, value); - } + private int _bucketId; + [Required] + public int BucketId + { + get => _bucketId; + set => Set(ref _bucketId, value); + } - private decimal _amount; - [Column(TypeName = "decimal(65, 2)")] - public decimal Amount - { - get => _amount; - set => Set(ref _amount, value); - } + private decimal _amount; + [Column(TypeName = "decimal(65, 2)")] + public decimal Amount + { + get => _amount; + set => Set(ref _amount, value); } } diff --git a/OpenBudgeteer.Core/Models/ImportProfile.cs b/OpenBudgeteer.Core/Models/ImportProfile.cs index dfd3f40..c63ad16 100644 --- a/OpenBudgeteer.Core/Models/ImportProfile.cs +++ b/OpenBudgeteer.Core/Models/ImportProfile.cs @@ -1,95 +1,88 @@ -using System; -using System.Collections.Generic; -using System.Text; -using System.ComponentModel.DataAnnotations; -using System.ComponentModel.DataAnnotations.Schema; +namespace OpenBudgeteer.Core.Models; -namespace OpenBudgeteer.Core.Models +public class ImportProfile : BaseObject { - public class ImportProfile : BaseObject + private int _importProfileId; + public int ImportProfileId { - private int _importProfileId; - public int ImportProfileId - { - get => _importProfileId; - set => Set(ref _importProfileId, value); - } + get => _importProfileId; + set => Set(ref _importProfileId, value); + } - private string _profileName; - public string ProfileName - { - get => _profileName; - set => Set(ref _profileName, value); - } + private string _profileName; + public string ProfileName + { + get => _profileName; + set => Set(ref _profileName, value); + } - private int _accountId; - public int AccountId - { - get => _accountId; - set => Set(ref _accountId, value); - } + private int _accountId; + public int AccountId + { + get => _accountId; + set => Set(ref _accountId, value); + } - private int _headerRow; - public int HeaderRow - { - get => _headerRow; - set => Set(ref _headerRow, value); - } + private int _headerRow; + public int HeaderRow + { + get => _headerRow; + set => Set(ref _headerRow, value); + } - private char _delimiter; - public char Delimiter - { - get => _delimiter; - set => Set(ref _delimiter, value); - } + private char _delimiter; + public char Delimiter + { + get => _delimiter; + set => Set(ref _delimiter, value); + } - private char _textQualifier; - public char TextQualifier - { - get => _textQualifier; - set => Set(ref _textQualifier, value); - } + private char _textQualifier; + public char TextQualifier + { + get => _textQualifier; + set => Set(ref _textQualifier, value); + } - private string _dateFormat; - public string DateFormat - { - get => _dateFormat; - set => Set(ref _dateFormat, value); - } + private string _dateFormat; + public string DateFormat + { + get => _dateFormat; + set => Set(ref _dateFormat, value); + } - private string _numberFormat; - public string NumberFormat - { - get => _numberFormat; - set => Set(ref _numberFormat, value); - } + private string _numberFormat; + public string NumberFormat + { + get => _numberFormat; + set => Set(ref _numberFormat, value); + } - private string _transactionDateColumnName; - public string TransactionDateColumnName - { - get => _transactionDateColumnName; - set => Set(ref _transactionDateColumnName, value); - } + private string _transactionDateColumnName; + public string TransactionDateColumnName + { + get => _transactionDateColumnName; + set => Set(ref _transactionDateColumnName, value); + } - private string _payeeColumnName; - public string PayeeColumnName - { - get => _payeeColumnName; - set => Set(ref _payeeColumnName, value); - } + private string _payeeColumnName; + public string PayeeColumnName + { + get => _payeeColumnName; + set => Set(ref _payeeColumnName, value); + } - private string _memoColumnName; - public string MemoColumnName - { - get => _memoColumnName; - set => Set(ref _memoColumnName, value); - } + private string _memoColumnName; + public string MemoColumnName + { + get => _memoColumnName; + set => Set(ref _memoColumnName, value); + } - private string _amountColumnName; - public string AmountColumnName - { - get => _amountColumnName; - set => Set(ref _amountColumnName, value); - } + private string _amountColumnName; + public string AmountColumnName + { + get => _amountColumnName; + set => Set(ref _amountColumnName, value); } } diff --git a/OpenBudgeteer.Core/Models/MappingRule.cs b/OpenBudgeteer.Core/Models/MappingRule.cs index e644a40..4782e90 100644 --- a/OpenBudgeteer.Core/Models/MappingRule.cs +++ b/OpenBudgeteer.Core/Models/MappingRule.cs @@ -1,114 +1,110 @@ -using System; -using System.Collections.Generic; -using System.ComponentModel.DataAnnotations; +using System.ComponentModel.DataAnnotations; using System.ComponentModel.DataAnnotations.Schema; -using System.Text; -namespace OpenBudgeteer.Core.Models +namespace OpenBudgeteer.Core.Models; + +public class MappingRule : BaseObject { - public class MappingRule : BaseObject + private int _mappingRuleId; + public int MappingRuleId { - private int _mappingRuleId; - public int MappingRuleId - { - get => _mappingRuleId; - set => Set(ref _mappingRuleId, value); - } + get => _mappingRuleId; + set => Set(ref _mappingRuleId, value); + } - private int _bucketRuleSetId; - [Required] - public int BucketRuleSetId - { - get => _bucketRuleSetId; - set => Set(ref _bucketRuleSetId, value); - } + private int _bucketRuleSetId; + [Required] + public int BucketRuleSetId + { + get => _bucketRuleSetId; + set => Set(ref _bucketRuleSetId, value); + } - private int _comparisionField; - /// - /// field which should be compared - /// - /// 1 - - /// 2 - - /// 3 - - /// 4 - - /// - /// - [Required] - public int ComparisionField - { - get => _comparisionField; - set => Set(ref _comparisionField, value); - } + private int _comparisionField; + /// + /// field which should be compared + /// + /// 1 - + /// 2 - + /// 3 - + /// 4 - + /// + /// + [Required] + public int ComparisionField + { + get => _comparisionField; + set => Set(ref _comparisionField, value); + } - [NotMapped] - public string ComparisonFieldOutput + [NotMapped] + public string ComparisonFieldOutput + { + get { - get + switch (ComparisionField) { - switch (ComparisionField) - { - case 1: - return nameof(Account); - case 2: - return nameof(BankTransaction.Payee); - case 3: - return nameof(BankTransaction.Memo); - case 4: - return nameof(BankTransaction.Amount); - default: - return string.Empty; - } + case 1: + return nameof(Account); + case 2: + return nameof(BankTransaction.Payee); + case 3: + return nameof(BankTransaction.Memo); + case 4: + return nameof(BankTransaction.Amount); + default: + return string.Empty; } } + } - private int _comparisionType; - /// - /// Identifier how Comparison should happen - /// - /// 1 - Equal - /// 2 - Not Equal - /// 3 - Contains - /// 4 - Does not contain - /// - /// - [Required] - public int ComparisionType - { - get => _comparisionType; - set => Set(ref _comparisionType, value); - } + private int _comparisionType; + /// + /// Identifier how Comparison should happen + /// + /// 1 - Equal + /// 2 - Not Equal + /// 3 - Contains + /// 4 - Does not contain + /// + /// + [Required] + public int ComparisionType + { + get => _comparisionType; + set => Set(ref _comparisionType, value); + } - [NotMapped] - public string ComparisionTypeOutput + [NotMapped] + public string ComparisionTypeOutput + { + get { - get + switch (ComparisionType) { - switch (ComparisionType) - { - case 1: - return "equal"; - case 2: - return "not equal"; - case 3: - return "contains"; - case 4: - return "does not contain"; - default: - return string.Empty; - } + case 1: + return "equal"; + case 2: + return "not equal"; + case 3: + return "contains"; + case 4: + return "does not contain"; + default: + return string.Empty; } } + } - private string _comparisionValue; - /// - /// Value of the field that needs to be compared - /// - [Required] - public string ComparisionValue - { - get => _comparisionValue; - set => Set(ref _comparisionValue, value); - } - + private string _comparisionValue; + /// + /// Value of the field that needs to be compared + /// + [Required] + public string ComparisionValue + { + get => _comparisionValue; + set => Set(ref _comparisionValue, value); } + } diff --git a/OpenBudgeteer.Core/OpenBudgeteer.Core.csproj b/OpenBudgeteer.Core/OpenBudgeteer.Core.csproj index 2ef1342..e844703 100644 --- a/OpenBudgeteer.Core/OpenBudgeteer.Core.csproj +++ b/OpenBudgeteer.Core/OpenBudgeteer.Core.csproj @@ -1,7 +1,7 @@ - net5.0 + net6.0 diff --git a/OpenBudgeteer.Core/ViewModels/AccountViewModel.cs b/OpenBudgeteer.Core/ViewModels/AccountViewModel.cs index 59a94fd..558974e 100644 --- a/OpenBudgeteer.Core/ViewModels/AccountViewModel.cs +++ b/OpenBudgeteer.Core/ViewModels/AccountViewModel.cs @@ -1,104 +1,98 @@ using System; -using System.Collections.Generic; using System.Collections.ObjectModel; -using System.ComponentModel; using System.Linq; -using System.Text; -using System.Windows; using Microsoft.EntityFrameworkCore; using OpenBudgeteer.Core.Common; using OpenBudgeteer.Core.Common.Database; -using OpenBudgeteer.Core.Common.EventClasses; using OpenBudgeteer.Core.Models; using OpenBudgeteer.Core.ViewModels.ItemViewModels; -namespace OpenBudgeteer.Core.ViewModels +namespace OpenBudgeteer.Core.ViewModels; + +public class AccountViewModel : ViewModelBase { - public class AccountViewModel : ViewModelBase + private ObservableCollection _accounts; + /// + /// Collection of ViewModelItems for Model + /// + public ObservableCollection Accounts { - private ObservableCollection _accounts; - /// - /// Collection of ViewModelItems for Model - /// - public ObservableCollection Accounts - { - get => _accounts; - set => Set(ref _accounts, value); - } + get => _accounts; + set => Set(ref _accounts, value); + } - private readonly DbContextOptions _dbOptions; + private readonly DbContextOptions _dbOptions; - /// - /// Basic constructor - /// - /// Options to connect to a database - public AccountViewModel(DbContextOptions dbOptions) - { - _dbOptions = dbOptions; - Accounts = new ObservableCollection(); - } + /// + /// Basic constructor + /// + /// Options to connect to a database + public AccountViewModel(DbContextOptions dbOptions) + { + _dbOptions = dbOptions; + Accounts = new ObservableCollection(); + } - /// - /// Initialize ViewModel and load data from database - /// - public ViewModelOperationResult LoadData() + /// + /// Initialize ViewModel and load data from database + /// + public ViewModelOperationResult LoadData() + { + try { - try + Accounts.Clear(); + using (var accountDbContext = new DatabaseContext(_dbOptions)) { - Accounts.Clear(); - using (var accountDbContext = new DatabaseContext(_dbOptions)) + foreach (var account in accountDbContext.Account + .Where(i => i.IsActive == 1) + .OrderBy(i => i.Name)) { - foreach (var account in accountDbContext.Account - .Where(i => i.IsActive == 1) - .OrderBy(i => i.Name)) + var newAccountItem = new AccountViewModelItem(_dbOptions, account); + decimal newIn = 0; + decimal newOut = 0; + + using (var transactionDbContext = new DatabaseContext(_dbOptions)) { - var newAccountItem = new AccountViewModelItem(_dbOptions, account); - decimal newIn = 0; - decimal newOut = 0; + var transactions = transactionDbContext.BankTransaction + .Where(i => i.AccountId == account.AccountId); - using (var transactionDbContext = new DatabaseContext(_dbOptions)) + foreach (var transaction in transactions) { - var transactions = transactionDbContext.BankTransaction - .Where(i => i.AccountId == account.AccountId); - - foreach (var transaction in transactions) - { - if (transaction.Amount > 0) - newIn += transaction.Amount; - else - newOut += transaction.Amount; - } + if (transaction.Amount > 0) + newIn += transaction.Amount; + else + newOut += transaction.Amount; } + } - newAccountItem.Balance = newIn + newOut; - newAccountItem.In = newIn; - newAccountItem.Out = newOut; + newAccountItem.Balance = newIn + newOut; + newAccountItem.In = newIn; + newAccountItem.Out = newOut; - Accounts.Add(newAccountItem); - } + Accounts.Add(newAccountItem); } } - catch (Exception e) - { - return new ViewModelOperationResult(false, $"Error during loading: {e.Message}"); - } - return new ViewModelOperationResult(true); } - - /// - /// Creates an initial which can be used for further manipulation - /// - /// Newly initialized - public AccountViewModelItem PrepareNewAccount() + catch (Exception e) { - var result = new AccountViewModelItem(_dbOptions) - { - Account = new Account { AccountId = 0, Name = "New Account", IsActive = 1 }, - Balance = 0, - In = 0, - Out = 0 - }; - return result; + return new ViewModelOperationResult(false, $"Error during loading: {e.Message}"); } + return new ViewModelOperationResult(true); + } + + /// + /// Creates an initial which can be used for further manipulation + /// + /// Newly initialized + public AccountViewModelItem PrepareNewAccount() + { + var result = new AccountViewModelItem(_dbOptions) + { + Account = new Account { AccountId = 0, Name = "New Account", IsActive = 1 }, + Balance = 0, + In = 0, + Out = 0 + }; + return result; } } diff --git a/OpenBudgeteer.Core/ViewModels/BucketViewModel.cs b/OpenBudgeteer.Core/ViewModels/BucketViewModel.cs index 789be33..ffe3a2d 100644 --- a/OpenBudgeteer.Core/ViewModels/BucketViewModel.cs +++ b/OpenBudgeteer.Core/ViewModels/BucketViewModel.cs @@ -1,401 +1,394 @@ using System; using System.Collections.Generic; using System.Collections.ObjectModel; -using System.ComponentModel; using System.Linq; -using System.Text; -using System.Text.RegularExpressions; -using System.Windows; using OpenBudgeteer.Core.Common.Database; using OpenBudgeteer.Core.Models; using OpenBudgeteer.Core.ViewModels.ItemViewModels; using Microsoft.EntityFrameworkCore; using System.Threading.Tasks; -using Microsoft.EntityFrameworkCore.Internal; using OpenBudgeteer.Core.Common; -using OpenBudgeteer.Core.Common.EventClasses; -namespace OpenBudgeteer.Core.ViewModels +namespace OpenBudgeteer.Core.ViewModels; + +public class BucketViewModel : ViewModelBase { - public class BucketViewModel : ViewModelBase + private decimal _income; + /// + /// Money that has been added to a Bucket + /// + public decimal Income { - private decimal _income; - /// - /// Money that has been added to a Bucket - /// - public decimal Income - { - get => _income; - private set => Set(ref _income, value); - } + get => _income; + private set => Set(ref _income, value); + } - private decimal _expenses; - /// - /// Money that has been moved out of the Bucket - /// - public decimal Expenses - { - get => _expenses; - private set => Set(ref _expenses, value); - } + private decimal _expenses; + /// + /// Money that has been moved out of the Bucket + /// + public decimal Expenses + { + get => _expenses; + private set => Set(ref _expenses, value); + } - private decimal _monthBalance; - /// - /// Combined Income and Expenses in a specific month - /// - public decimal MonthBalance - { - get => _monthBalance; - private set => Set(ref _monthBalance, value); - } + private decimal _monthBalance; + /// + /// Combined Income and Expenses in a specific month + /// + public decimal MonthBalance + { + get => _monthBalance; + private set => Set(ref _monthBalance, value); + } - private decimal _budget; - /// - /// Available Money in a specific month - /// - public decimal Budget - { - get => _budget; - private set => Set(ref _budget, value); - } + private decimal _budget; + /// + /// Available Money in a specific month + /// + public decimal Budget + { + get => _budget; + private set => Set(ref _budget, value); + } - private decimal _bankBalance; - /// - /// Money available on all bank accounts - /// - public decimal BankBalance - { - get => _bankBalance; - private set => Set(ref _bankBalance, value); - } + private decimal _bankBalance; + /// + /// Money available on all bank accounts + /// + public decimal BankBalance + { + get => _bankBalance; + private set => Set(ref _bankBalance, value); + } - private decimal _pendingWant; - /// - /// Money expected to be added to a Bucket in a specific month - /// - public decimal PendingWant - { - get => _pendingWant; - private set => Set(ref _pendingWant, value); - } + private decimal _pendingWant; + /// + /// Money expected to be added to a Bucket in a specific month + /// + public decimal PendingWant + { + get => _pendingWant; + private set => Set(ref _pendingWant, value); + } - private decimal _remainingBudget; - /// - /// Remaining Money in a specific month. Includes Want and negative Balances - /// - public decimal RemainingBudget - { - get => _remainingBudget; - private set => Set(ref _remainingBudget, value); - } + private decimal _remainingBudget; + /// + /// Remaining Money in a specific month. Includes Want and negative Balances + /// + public decimal RemainingBudget + { + get => _remainingBudget; + private set => Set(ref _remainingBudget, value); + } - private decimal _negativeBucketBalance; - /// - /// Sum of all Bucket Balances where the number is negative - /// - public decimal NegativeBucketBalance - { - get => _negativeBucketBalance; - private set => Set(ref _negativeBucketBalance, value); - } + private decimal _negativeBucketBalance; + /// + /// Sum of all Bucket Balances where the number is negative + /// + public decimal NegativeBucketBalance + { + get => _negativeBucketBalance; + private set => Set(ref _negativeBucketBalance, value); + } - private ObservableCollection _bucketGroups; - /// - /// Collection of Groups which contains a set of Buckets - /// - public ObservableCollection BucketGroups - { - get => _bucketGroups; - private set => Set(ref _bucketGroups, value); - } + private ObservableCollection _bucketGroups; + /// + /// Collection of Groups which contains a set of Buckets + /// + public ObservableCollection BucketGroups + { + get => _bucketGroups; + private set => Set(ref _bucketGroups, value); + } - private readonly DbContextOptions _dbOptions; - private readonly YearMonthSelectorViewModel _yearMonthViewModel; + private readonly DbContextOptions _dbOptions; + private readonly YearMonthSelectorViewModel _yearMonthViewModel; - private bool _defaultCollapseState; // Keep Collapse State e.g. after YearMonth change of ViewModel reload + private bool _defaultCollapseState; // Keep Collapse State e.g. after YearMonth change of ViewModel reload - /// - /// Basic constructor - /// - /// Options to connect to a database - /// ViewModel instance to handle selection of a year and month - public BucketViewModel(DbContextOptions dbOptions, YearMonthSelectorViewModel yearMonthViewModel) - { - _dbOptions = dbOptions; - BucketGroups = new ObservableCollection(); - _yearMonthViewModel = yearMonthViewModel; - //_yearMonthViewModel.SelectedYearMonthChanged += (sender) => { LoadData(); }; - } + /// + /// Basic constructor + /// + /// Options to connect to a database + /// ViewModel instance to handle selection of a year and month + public BucketViewModel(DbContextOptions dbOptions, YearMonthSelectorViewModel yearMonthViewModel) + { + _dbOptions = dbOptions; + BucketGroups = new ObservableCollection(); + _yearMonthViewModel = yearMonthViewModel; + //_yearMonthViewModel.SelectedYearMonthChanged += (sender) => { LoadData(); }; + } - /// - /// Initialize ViewModel and load data from database - /// - /// Object which contains information and results of this method - public async Task LoadDataAsync() + /// + /// Initialize ViewModel and load data from database + /// + /// Object which contains information and results of this method + public async Task LoadDataAsync() + { + try { - try + BucketGroups.Clear(); + using (var dbContext = new DatabaseContext(_dbOptions)) { - BucketGroups.Clear(); - using (var dbContext = new DatabaseContext(_dbOptions)) + var bucketGroups = dbContext.BucketGroup + .OrderBy(i => i.Position) + .ToList(); + + foreach (var bucketGroup in bucketGroups) { - var bucketGroups = dbContext.BucketGroup - .OrderBy(i => i.Position) - .ToList(); + var newBucketGroup = new BucketGroupViewModelItem(_dbOptions, bucketGroup, _yearMonthViewModel.CurrentMonth); + newBucketGroup.IsCollapsed = _defaultCollapseState; + var buckets = dbContext.Bucket + .Where(i => i.BucketGroupId == newBucketGroup.BucketGroup.BucketGroupId) + .OrderBy(i => i.Name) + .ToList(); + + var bucketItemTasks = new List>(); + + foreach (var bucket in buckets) + { + if (bucket.ValidFrom > _yearMonthViewModel.CurrentMonth) continue; // Bucket not yet active for selected month + if (bucket.IsInactive && bucket.IsInactiveFrom <= _yearMonthViewModel.CurrentMonth) continue; // Bucket no longer active for selected month + bucketItemTasks.Add(BucketViewModelItem.CreateAsync(_dbOptions, bucket, _yearMonthViewModel.CurrentMonth)); + } - foreach (var bucketGroup in bucketGroups) + foreach (var bucket in await Task.WhenAll(bucketItemTasks)) { - var newBucketGroup = new BucketGroupViewModelItem(_dbOptions, bucketGroup, _yearMonthViewModel.CurrentMonth); - newBucketGroup.IsCollapsed = _defaultCollapseState; - var buckets = dbContext.Bucket - .Where(i => i.BucketGroupId == newBucketGroup.BucketGroup.BucketGroupId) - .OrderBy(i => i.Name) - .ToList(); - - var bucketItemTasks = new List>(); - - foreach (var bucket in buckets) - { - if (bucket.ValidFrom > _yearMonthViewModel.CurrentMonth) continue; // Bucket not yet active for selected month - if (bucket.IsInactive && bucket.IsInactiveFrom <= _yearMonthViewModel.CurrentMonth) continue; // Bucket no longer active for selected month - bucketItemTasks.Add(BucketViewModelItem.CreateAsync(_dbOptions, bucket, _yearMonthViewModel.CurrentMonth)); - } - - foreach (var bucket in await Task.WhenAll(bucketItemTasks)) - { - newBucketGroup.Buckets.Add(bucket); - } - BucketGroups.Add(newBucketGroup); + newBucketGroup.Buckets.Add(bucket); } + BucketGroups.Add(newBucketGroup); } - var result = UpdateBalanceFigures(); - if (!result.IsSuccessful) throw new Exception(result.Message); } - catch (Exception e) + var result = UpdateBalanceFigures(); + if (!result.IsSuccessful) throw new Exception(result.Message); + } + catch (Exception e) + { + return new ViewModelOperationResult(false, $"Error during loading: {e.Message}"); + } + return new ViewModelOperationResult(true); + } + + /// + /// Creates an initial and adds it to ViewModel and Database. + /// Will be added on first position and updates all other Positions accordingly + /// + /// Object which contains information and results of this method + public ViewModelOperationResult CreateGroup() + { + var newGroup = new BucketGroup + { + BucketGroupId = 0, + Name = "New Bucket Group", + Position = 1 + }; + using (var dbContext = new DatabaseContext(_dbOptions)) + { + foreach (var bucketGroup in BucketGroups) { - return new ViewModelOperationResult(false, $"Error during loading: {e.Message}"); + bucketGroup.BucketGroup.Position++; + dbContext.UpdateBucketGroup(bucketGroup.BucketGroup); } - return new ViewModelOperationResult(true); + if (dbContext.CreateBucketGroup(newGroup) == 0) return new ViewModelOperationResult(false, "Unable to write changes to database"); } - /// - /// Creates an initial and adds it to ViewModel and Database. - /// Will be added on first position and updates all other Positions accordingly - /// - /// Object which contains information and results of this method - public ViewModelOperationResult CreateGroup() - { - var newGroup = new BucketGroup + var newBucketGroupViewModelItem = + new BucketGroupViewModelItem(_dbOptions, newGroup, _yearMonthViewModel.CurrentMonth) { - BucketGroupId = 0, - Name = "New Bucket Group", - Position = 1 + InModification = true }; - using (var dbContext = new DatabaseContext(_dbOptions)) - { - foreach (var bucketGroup in BucketGroups) - { - bucketGroup.BucketGroup.Position++; - dbContext.UpdateBucketGroup(bucketGroup.BucketGroup); - } - if (dbContext.CreateBucketGroup(newGroup) == 0) return new ViewModelOperationResult(false, "Unable to write changes to database"); - } + BucketGroups.Insert(0, newBucketGroupViewModelItem); + return new ViewModelOperationResult(true); + } - var newBucketGroupViewModelItem = - new BucketGroupViewModelItem(_dbOptions, newGroup, _yearMonthViewModel.CurrentMonth) - { - InModification = true - }; - BucketGroups.Insert(0, newBucketGroupViewModelItem); - return new ViewModelOperationResult(true); - } + /// + /// Starts deletion process in the passed and updates positions of + /// all other accordingly + /// + /// Triggers + /// Instance that needs to be deleted + /// Object which contains information and results of this method + public ViewModelOperationResult DeleteGroup(BucketGroupViewModelItem bucketGroup) + { + var index = BucketGroups.IndexOf(bucketGroup) + 1; + var bucketGroupsToMove = BucketGroups.ToList().GetRange(index, BucketGroups.Count - index); - /// - /// Starts deletion process in the passed and updates positions of - /// all other accordingly - /// - /// Triggers - /// Instance that needs to be deleted - /// Object which contains information and results of this method - public ViewModelOperationResult DeleteGroup(BucketGroupViewModelItem bucketGroup) + using (var dbContext = new DatabaseContext(_dbOptions)) { - var index = BucketGroups.IndexOf(bucketGroup) + 1; - var bucketGroupsToMove = BucketGroups.ToList().GetRange(index, BucketGroups.Count - index); - - using (var dbContext = new DatabaseContext(_dbOptions)) + using (var transaction = dbContext.Database.BeginTransaction()) { - using (var transaction = dbContext.Database.BeginTransaction()) + try { - try + if (bucketGroup.Buckets.Count > 0) throw new Exception("Groups with Buckets cannot be deleted."); + dbContext.DeleteBucketGroup(bucketGroup.BucketGroup); + + var dbBucketGroups = new List(); + foreach (var bucketGroupViewModelItem in bucketGroupsToMove) { - if (bucketGroup.Buckets.Count > 0) throw new Exception("Groups with Buckets cannot be deleted."); - dbContext.DeleteBucketGroup(bucketGroup.BucketGroup); - - var dbBucketGroups = new List(); - foreach (var bucketGroupViewModelItem in bucketGroupsToMove) - { - bucketGroupViewModelItem.BucketGroup.Position -= 1; - dbBucketGroups.Add(bucketGroupViewModelItem.BucketGroup); - } - - dbContext.UpdateBucketGroups(dbBucketGroups); - - transaction.Commit(); - return new ViewModelOperationResult(true, true); - } - catch (Exception e) - { - transaction.Rollback(); - return new ViewModelOperationResult(false, e.Message); + bucketGroupViewModelItem.BucketGroup.Position -= 1; + dbBucketGroups.Add(bucketGroupViewModelItem.BucketGroup); } + + dbContext.UpdateBucketGroups(dbBucketGroups); + + transaction.Commit(); + return new ViewModelOperationResult(true, true); + } + catch (Exception e) + { + transaction.Rollback(); + return new ViewModelOperationResult(false, e.Message); } } } + } - /// - /// Put money into all Buckets according to their Want. Saves the results to the database. - /// - /// Doesn't consider any available Budget figures. - /// Triggers - /// Object which contains information and results of this method - public ViewModelOperationResult DistributeBudget() + /// + /// Put money into all Buckets according to their Want. Saves the results to the database. + /// + /// Doesn't consider any available Budget figures. + /// Triggers + /// Object which contains information and results of this method + public ViewModelOperationResult DistributeBudget() + { + using (var dbContext = new DatabaseContext(_dbOptions)) { - using (var dbContext = new DatabaseContext(_dbOptions)) + using (var transaction = dbContext.Database.BeginTransaction()) { - using (var transaction = dbContext.Database.BeginTransaction()) + try { - try + var buckets = new List(); + foreach (var bucketGroup in BucketGroups) { - var buckets = new List(); - foreach (var bucketGroup in BucketGroups) - { - buckets.AddRange(bucketGroup.Buckets); - } - foreach (var bucket in buckets) - { - if (bucket.Want == 0) continue; - bucket.InOut = bucket.Want; - var result = bucket.HandleInOutInput("Enter"); - if (!result.IsSuccessful) throw new Exception(result.Message); - } - - transaction.Commit(); - //UpdateBalanceFigures(); // Should be done but not required because it will be done during ViewModel reload - return new ViewModelOperationResult(true, true); + buckets.AddRange(bucketGroup.Buckets); } - catch (Exception e) + foreach (var bucket in buckets) { - transaction.Rollback(); - return new ViewModelOperationResult(false, $"Error during Budget distribution: {e.Message}"); + if (bucket.Want == 0) continue; + bucket.InOut = bucket.Want; + var result = bucket.HandleInOutInput("Enter"); + if (!result.IsSuccessful) throw new Exception(result.Message); } - } - } - } - /// - /// Re-calculates figures of the ViewModel like Budget and Balances - /// - /// Object which contains information and results of this method - public ViewModelOperationResult UpdateBalanceFigures() - { - try - { - var buckets = new List(); - foreach (var bucketGroup in BucketGroups) - { - bucketGroup.TotalBalance = bucketGroup.Buckets.Sum(i => i.Balance); - buckets.AddRange(bucketGroup.Buckets); + transaction.Commit(); + //UpdateBalanceFigures(); // Should be done but not required because it will be done during ViewModel reload + return new ViewModelOperationResult(true, true); } - - using (var dbContext = new DatabaseContext(_dbOptions)) + catch (Exception e) { - // Get all Transactions which are not marked as "Transfer" for current YearMonth - var results = dbContext.BankTransaction - .Join( - dbContext.BudgetedTransaction, - i => i.TransactionId, - j => j.TransactionId, - (bankTransaction, budgetedTransaction) => new { bankTransaction, budgetedTransaction }) - .Where(i => - i.budgetedTransaction.BucketId != 2 && - i.bankTransaction.TransactionDate.Year == _yearMonthViewModel.SelectedYear && - i.bankTransaction.TransactionDate.Month == _yearMonthViewModel.SelectedMonth) - .Select(i => i.budgetedTransaction) - .ToList(); - - Income = results - .Where(i => i.Amount > 0) - .Sum(i => i.Amount); - - Expenses = results - .Where(i => i.Amount < 0) - .Sum(i => i.Amount); - - MonthBalance = Income + Expenses; - BankBalance = dbContext.BankTransaction - .Where(i => i.TransactionDate < _yearMonthViewModel.CurrentMonth.AddMonths(1)) - .ToList() - .Sum(i => i.Amount); - - - Budget = BankBalance - BucketGroups.Sum(i => i.TotalBalance); - - PendingWant = buckets.Where(i => i.Want > 0).Sum(i => i.Want); - RemainingBudget = Budget - PendingWant; - NegativeBucketBalance = buckets.Where(i => i.Balance < 0).Sum(i => i.Balance); + transaction.Rollback(); + return new ViewModelOperationResult(false, $"Error during Budget distribution: {e.Message}"); } } - catch (Exception e) - { - return new ViewModelOperationResult(false, $"Error during Balance recalculation: {e.Message}"); - } - - return new ViewModelOperationResult(true); } + } - /// - /// Helper method to set Collapse status for all - /// - /// New collapse status - public void ChangeBucketGroupCollapse(bool collapse = true) + /// + /// Re-calculates figures of the ViewModel like Budget and Balances + /// + /// Object which contains information and results of this method + public ViewModelOperationResult UpdateBalanceFigures() + { + try { - _defaultCollapseState = collapse; + var buckets = new List(); foreach (var bucketGroup in BucketGroups) { - bucketGroup.IsCollapsed = collapse; + bucketGroup.TotalBalance = bucketGroup.Buckets.Sum(i => i.Balance); + buckets.AddRange(bucketGroup.Buckets); } - } - /// - /// Helper method to start Save process for the passed - /// - /// Triggers also update of ViewModel figures - /// instance with modifications - /// Object which contains information and results of this method - public ViewModelOperationResult SaveChanges(BucketViewModelItem bucket) + using (var dbContext = new DatabaseContext(_dbOptions)) + { + // Get all Transactions which are not marked as "Transfer" for current YearMonth + var results = dbContext.BankTransaction + .Join( + dbContext.BudgetedTransaction, + i => i.TransactionId, + j => j.TransactionId, + (bankTransaction, budgetedTransaction) => new { bankTransaction, budgetedTransaction }) + .Where(i => + i.budgetedTransaction.BucketId != 2 && + i.bankTransaction.TransactionDate.Year == _yearMonthViewModel.SelectedYear && + i.bankTransaction.TransactionDate.Month == _yearMonthViewModel.SelectedMonth) + .Select(i => i.budgetedTransaction) + .ToList(); + + Income = results + .Where(i => i.Amount > 0) + .Sum(i => i.Amount); + + Expenses = results + .Where(i => i.Amount < 0) + .Sum(i => i.Amount); + + MonthBalance = Income + Expenses; + BankBalance = dbContext.BankTransaction + .Where(i => i.TransactionDate < _yearMonthViewModel.CurrentMonth.AddMonths(1)) + .ToList() + .Sum(i => i.Amount); + + + Budget = BankBalance - BucketGroups.Sum(i => i.TotalBalance); + + PendingWant = buckets.Where(i => i.Want > 0).Sum(i => i.Want); + RemainingBudget = Budget - PendingWant; + NegativeBucketBalance = buckets.Where(i => i.Balance < 0).Sum(i => i.Balance); + } + } + catch (Exception e) { - var createUpdateResult = bucket.CreateOrUpdateBucket(); - if (!createUpdateResult.IsSuccessful) return createUpdateResult; - var updateFiguresResult = UpdateBalanceFigures(); - return new ViewModelOperationResult( - updateFiguresResult.IsSuccessful, - createUpdateResult.ViewModelReloadRequired || updateFiguresResult.ViewModelReloadRequired); + return new ViewModelOperationResult(false, $"Error during Balance recalculation: {e.Message}"); } - /// - /// Helper method to start Deletion process for the passed - /// - /// Triggers also update of ViewModel figures - /// Triggers - /// instance with containing to be closed - /// Object which contains information and results of this method - public ViewModelOperationResult CloseBucket(BucketViewModelItem bucket) + return new ViewModelOperationResult(true); + } + + /// + /// Helper method to set Collapse status for all + /// + /// New collapse status + public void ChangeBucketGroupCollapse(bool collapse = true) + { + _defaultCollapseState = collapse; + foreach (var bucketGroup in BucketGroups) { - var closeBucketResult = bucket.CloseBucket(); - if (!closeBucketResult.IsSuccessful) return closeBucketResult; - var updateFiguresResult = UpdateBalanceFigures(); - return new ViewModelOperationResult( - updateFiguresResult.IsSuccessful, - closeBucketResult.ViewModelReloadRequired || updateFiguresResult.ViewModelReloadRequired); + bucketGroup.IsCollapsed = collapse; } } + + /// + /// Helper method to start Save process for the passed + /// + /// Triggers also update of ViewModel figures + /// instance with modifications + /// Object which contains information and results of this method + public ViewModelOperationResult SaveChanges(BucketViewModelItem bucket) + { + var createUpdateResult = bucket.CreateOrUpdateBucket(); + if (!createUpdateResult.IsSuccessful) return createUpdateResult; + var updateFiguresResult = UpdateBalanceFigures(); + return new ViewModelOperationResult( + updateFiguresResult.IsSuccessful, + createUpdateResult.ViewModelReloadRequired || updateFiguresResult.ViewModelReloadRequired); + } + + /// + /// Helper method to start Deletion process for the passed + /// + /// Triggers also update of ViewModel figures + /// Triggers + /// instance with containing to be closed + /// Object which contains information and results of this method + public ViewModelOperationResult CloseBucket(BucketViewModelItem bucket) + { + var closeBucketResult = bucket.CloseBucket(); + if (!closeBucketResult.IsSuccessful) return closeBucketResult; + var updateFiguresResult = UpdateBalanceFigures(); + return new ViewModelOperationResult( + updateFiguresResult.IsSuccessful, + closeBucketResult.ViewModelReloadRequired || updateFiguresResult.ViewModelReloadRequired); + } } diff --git a/OpenBudgeteer.Core/ViewModels/ImportDataViewModel.cs b/OpenBudgeteer.Core/ViewModels/ImportDataViewModel.cs index 74886a7..67a49ee 100644 --- a/OpenBudgeteer.Core/ViewModels/ImportDataViewModel.cs +++ b/OpenBudgeteer.Core/ViewModels/ImportDataViewModel.cs @@ -4,7 +4,6 @@ using System; using System.Collections.Generic; using System.Collections.ObjectModel; -using System.ComponentModel.DataAnnotations; using System.Globalization; using System.IO; using System.Linq; @@ -16,511 +15,510 @@ using TinyCsvParser.Tokenizer.RFC4180; using TinyCsvParser.TypeConverter; -namespace OpenBudgeteer.Core.ViewModels +namespace OpenBudgeteer.Core.ViewModels; + +public class ImportDataViewModel : ViewModelBase { - public class ImportDataViewModel : ViewModelBase + private class CsvBankTransactionMapping : CsvMapping { - private class CsvBankTransactionMapping : CsvMapping - { - /// - /// Definition on how CSV columns should be mapped to - /// - /// - /// instance and collection of columns will be used to identify columnIndex for - /// CSV mapping - /// - /// Instance required for CSV column name - /// Collection of all CSV columns - public CsvBankTransactionMapping(ImportProfile importProfile, IEnumerable identifiedColumns) : base() - { - // TODO Add User Input for CultureInfo for Amount & TransactionDate conversion - - MapProperty(identifiedColumns.ToList().IndexOf(importProfile.AmountColumnName), x => x.Amount, new DecimalConverter(new CultureInfo(importProfile.NumberFormat))); - MapProperty(identifiedColumns.ToList().IndexOf(importProfile.MemoColumnName), x => x.Memo); - if (!string.IsNullOrEmpty(importProfile.PayeeColumnName)) - { - MapProperty(identifiedColumns.ToList().IndexOf(importProfile.PayeeColumnName), x => x.Payee); - } - MapProperty(identifiedColumns.ToList().IndexOf(importProfile.TransactionDateColumnName), x => x.TransactionDate, new DateTimeConverter(importProfile.DateFormat)); - } - } - - private string _filePath; /// - /// Path to the file which should be imported + /// Definition on how CSV columns should be mapped to /// - public string FilePath + /// + /// instance and collection of columns will be used to identify columnIndex for + /// CSV mapping + /// + /// Instance required for CSV column name + /// Collection of all CSV columns + public CsvBankTransactionMapping(ImportProfile importProfile, IEnumerable identifiedColumns) : base() { - get => _filePath; - set => Set(ref _filePath, value); - } + // TODO Add User Input for CultureInfo for Amount & TransactionDate conversion - private string _fileText; - /// - /// Readonly content of the file - /// - public string FileText - { - get => _fileText; - set => Set(ref _fileText, value); + MapProperty(identifiedColumns.ToList().IndexOf(importProfile.AmountColumnName), x => x.Amount, new DecimalConverter(new CultureInfo(importProfile.NumberFormat))); + MapProperty(identifiedColumns.ToList().IndexOf(importProfile.MemoColumnName), x => x.Memo); + if (!string.IsNullOrEmpty(importProfile.PayeeColumnName)) + { + MapProperty(identifiedColumns.ToList().IndexOf(importProfile.PayeeColumnName), x => x.Payee); + } + MapProperty(identifiedColumns.ToList().IndexOf(importProfile.TransactionDateColumnName), x => x.TransactionDate, new DateTimeConverter(importProfile.DateFormat)); } + } - private Account _selectedAccount; - /// - /// Target for which all imported should be added - /// - public Account SelectedAccount - { - get => _selectedAccount; - set => Set(ref _selectedAccount, value); - } + private string _filePath; + /// + /// Path to the file which should be imported + /// + public string FilePath + { + get => _filePath; + set => Set(ref _filePath, value); + } - private ImportProfile _selectedImportProfile; - /// - /// Selected profile with import settings from the database - /// - public ImportProfile SelectedImportProfile - { - get => _selectedImportProfile; - set => Set(ref _selectedImportProfile, value); - } + private string _fileText; + /// + /// Readonly content of the file + /// + public string FileText + { + get => _fileText; + set => Set(ref _fileText, value); + } - private int _totalRecords; - /// - /// Number of records identified in the file - /// - public int TotalRecords - { - get => _totalRecords; - private set => Set(ref _totalRecords, value); - } + private Account _selectedAccount; + /// + /// Target for which all imported should be added + /// + public Account SelectedAccount + { + get => _selectedAccount; + set => Set(ref _selectedAccount, value); + } - private int _recordsWithErrors; - /// - /// Number of records where import will fail or has failed - /// - public int RecordsWithErrors - { - get => _recordsWithErrors; - private set => Set(ref _recordsWithErrors, value); - } + private ImportProfile _selectedImportProfile; + /// + /// Selected profile with import settings from the database + /// + public ImportProfile SelectedImportProfile + { + get => _selectedImportProfile; + set => Set(ref _selectedImportProfile, value); + } - private int _validRecords; - /// - /// Number of records where import will be or was successful - /// - public int ValidRecords - { - get => _validRecords; - private set => Set(ref _validRecords, value); - } + private int _totalRecords; + /// + /// Number of records identified in the file + /// + public int TotalRecords + { + get => _totalRecords; + private set => Set(ref _totalRecords, value); + } - private ObservableCollection _availableImportProfiles; - /// - /// Available in the database - /// - public ObservableCollection AvailableImportProfiles - { - get => _availableImportProfiles; - private set => Set(ref _availableImportProfiles, value); - } + private int _recordsWithErrors; + /// + /// Number of records where import will fail or has failed + /// + public int RecordsWithErrors + { + get => _recordsWithErrors; + private set => Set(ref _recordsWithErrors, value); + } - private ObservableCollection _availableAccounts; - /// - /// Helper collection to list all available in the database - /// - public ObservableCollection AvailableAccounts - { - get => _availableAccounts; - private set => Set(ref _availableAccounts, value); - } + private int _validRecords; + /// + /// Number of records where import will be or was successful + /// + public int ValidRecords + { + get => _validRecords; + private set => Set(ref _validRecords, value); + } - private ObservableCollection _identifiedColumns; - /// - /// Collection of columns that have been identified in the CSV file - /// - public ObservableCollection IdentifiedColumns - { - get => _identifiedColumns; - private set => Set(ref _identifiedColumns, value); - } + private ObservableCollection _availableImportProfiles; + /// + /// Available in the database + /// + public ObservableCollection AvailableImportProfiles + { + get => _availableImportProfiles; + private set => Set(ref _availableImportProfiles, value); + } - private ObservableCollection> _parsedRecords; - /// - /// Results of - /// - public ObservableCollection> ParsedRecords - { - get => _parsedRecords; - private set => Set(ref _parsedRecords, value); - } - - private bool _isProfileValid; - private string[] _fileLines; - private readonly DbContextOptions _dbOptions; + private ObservableCollection _availableAccounts; + /// + /// Helper collection to list all available in the database + /// + public ObservableCollection AvailableAccounts + { + get => _availableAccounts; + private set => Set(ref _availableAccounts, value); + } - /// - /// Basic constructor - /// - /// Options to connect to a database - public ImportDataViewModel(DbContextOptions dbOptions) - { - AvailableImportProfiles = new ObservableCollection(); - AvailableAccounts = new ObservableCollection(); - IdentifiedColumns = new ObservableCollection(); - ParsedRecords = new ObservableCollection>(); - SelectedImportProfile = new ImportProfile(); - SelectedAccount = new Account(); - _dbOptions = dbOptions; - } + private ObservableCollection _identifiedColumns; + /// + /// Collection of columns that have been identified in the CSV file + /// + public ObservableCollection IdentifiedColumns + { + get => _identifiedColumns; + private set => Set(ref _identifiedColumns, value); + } - /// - /// Initialize ViewModel and load data from database - /// - /// - public ViewModelOperationResult LoadData() + private ObservableCollection> _parsedRecords; + /// + /// Results of + /// + public ObservableCollection> ParsedRecords + { + get => _parsedRecords; + private set => Set(ref _parsedRecords, value); + } + + private bool _isProfileValid; + private string[] _fileLines; + private readonly DbContextOptions _dbOptions; + + /// + /// Basic constructor + /// + /// Options to connect to a database + public ImportDataViewModel(DbContextOptions dbOptions) + { + AvailableImportProfiles = new ObservableCollection(); + AvailableAccounts = new ObservableCollection(); + IdentifiedColumns = new ObservableCollection(); + ParsedRecords = new ObservableCollection>(); + SelectedImportProfile = new ImportProfile(); + SelectedAccount = new Account(); + _dbOptions = dbOptions; + } + + /// + /// Initialize ViewModel and load data from database + /// + /// + public ViewModelOperationResult LoadData() + { + try { - try + LoadAvailableProfiles(); + using (var dbContext = new DatabaseContext(_dbOptions)) { - LoadAvailableProfiles(); - using (var dbContext = new DatabaseContext(_dbOptions)) + foreach (var account in dbContext.Account.Where(i => i.IsActive == 1)) { - foreach (var account in dbContext.Account.Where(i => i.IsActive == 1)) - { - AvailableAccounts.Add(account); - } + AvailableAccounts.Add(account); } - return new ViewModelOperationResult(true); } - catch (Exception e) - { - return new ViewModelOperationResult(false, $"Error during loading: {e.Message}"); - } + return new ViewModelOperationResult(true); } + catch (Exception e) + { + return new ViewModelOperationResult(false, $"Error during loading: {e.Message}"); + } + } - /// - /// Open a file based on and read its content - /// - /// Object which contains information and results of this method - public ViewModelOperationResult HandleOpenFile() + /// + /// Open a file based on and read its content + /// + /// Object which contains information and results of this method + public ViewModelOperationResult HandleOpenFile() + { + try { - try - { - FileText = File.ReadAllText(FilePath, Encoding.GetEncoding(1252)); - _fileLines = File.ReadAllLines(FilePath, Encoding.GetEncoding(1252)); + FileText = File.ReadAllText(FilePath, Encoding.GetEncoding(1252)); + _fileLines = File.ReadAllLines(FilePath, Encoding.GetEncoding(1252)); - return new ViewModelOperationResult(true); - } - catch (Exception e) - { - return new ViewModelOperationResult(false, $"Error during loading: {e.Message}"); - } + return new ViewModelOperationResult(true); } - - /// - /// Open a file based on results of an OpenFileDialog and read its content - /// - /// OpenFileDialog results - /// Object which contains information and results of this method - public ViewModelOperationResult HandleOpenFile(string[] dialogResults) + catch (Exception e) { - try - { - if (!dialogResults.Any()) return new ViewModelOperationResult(true); - FilePath = dialogResults[0]; - return HandleOpenFile(); - } - catch (Exception e) - { - return new ViewModelOperationResult(false, $"Error during loading: {e.Message}"); - } + return new ViewModelOperationResult(false, $"Error during loading: {e.Message}"); } + } - /// - /// Open a file based on a and read its content - /// - /// Stream to the file - /// Object which contains information and results of this method - public async Task HandleOpenFileAsync(Stream stream) + /// + /// Open a file based on results of an OpenFileDialog and read its content + /// + /// OpenFileDialog results + /// Object which contains information and results of this method + public ViewModelOperationResult HandleOpenFile(string[] dialogResults) + { + try { - try - { - string line; - var newLines = new List(); - var stringBuilder = new StringBuilder(); - - FilePath = string.Empty; + if (!dialogResults.Any()) return new ViewModelOperationResult(true); + FilePath = dialogResults[0]; + return HandleOpenFile(); + } + catch (Exception e) + { + return new ViewModelOperationResult(false, $"Error during loading: {e.Message}"); + } + } - using var lineReader = new StreamReader(stream, Encoding.GetEncoding(1252)); - while((line = await lineReader.ReadLineAsync()) != null) - { - newLines.Add(line); - stringBuilder.AppendLine(line); - } + /// + /// Open a file based on a and read its content + /// + /// Stream to the file + /// Object which contains information and results of this method + public async Task HandleOpenFileAsync(Stream stream) + { + try + { + string line; + var newLines = new List(); + var stringBuilder = new StringBuilder(); - FileText = stringBuilder.ToString(); - _fileLines = newLines.ToArray(); + FilePath = string.Empty; - return new ViewModelOperationResult(true); - } - catch (Exception e) + using var lineReader = new StreamReader(stream, Encoding.GetEncoding(1252)); + while((line = await lineReader.ReadLineAsync()) != null) { - return new ViewModelOperationResult(false, $"Unable to open file: {e.Message}"); + newLines.Add(line); + stringBuilder.AppendLine(line); } + + FileText = stringBuilder.ToString(); + _fileLines = newLines.ToArray(); + + return new ViewModelOperationResult(true); } + catch (Exception e) + { + return new ViewModelOperationResult(false, $"Unable to open file: {e.Message}"); + } + } - /// - /// Loads all settings based on - /// - /// Object which contains information and results of this method - public ViewModelOperationResult LoadProfile() + /// + /// Loads all settings based on + /// + /// Object which contains information and results of this method + public ViewModelOperationResult LoadProfile() + { + try { - try + ResetLoadedProfileData(); + + // Set target Account + if (AvailableAccounts.Any(i => i.AccountId == SelectedImportProfile.AccountId)) { - ResetLoadedProfileData(); + SelectedAccount = AvailableAccounts.First(i => i.AccountId == SelectedImportProfile.AccountId); + } - // Set target Account - if (AvailableAccounts.Any(i => i.AccountId == SelectedImportProfile.AccountId)) - { - SelectedAccount = AvailableAccounts.First(i => i.AccountId == SelectedImportProfile.AccountId); - } + var result = LoadHeaders(); + if (!result.IsSuccessful) throw new Exception(result.Message); - var result = LoadHeaders(); - if (!result.IsSuccessful) throw new Exception(result.Message); + ValidateData(); + _isProfileValid = true; - ValidateData(); - _isProfileValid = true; + return new ViewModelOperationResult(true); + } + catch (Exception e) + { + _isProfileValid = false; + return new ViewModelOperationResult(false, $"Unable to load Profile: {e.Message}"); + } + } - return new ViewModelOperationResult(true); - } - catch (Exception e) + /// + /// Reads column headers from the loaded file + /// + /// Object which contains information and results of this method + public ViewModelOperationResult LoadHeaders() + { + try + { + // Set ComboBox selection for Column Mapping + IdentifiedColumns.Clear(); + var headerLine = _fileLines[SelectedImportProfile.HeaderRow - 1]; + var columns = headerLine.Split(SelectedImportProfile.Delimiter); + foreach (var column in columns) { - _isProfileValid = false; - return new ViewModelOperationResult(false, $"Unable to load Profile: {e.Message}"); + if (column != string.Empty) + IdentifiedColumns.Add(column.Trim(SelectedImportProfile.TextQualifier)); // Exclude TextQualifier } + + return new ViewModelOperationResult(true); } + catch (Exception e) + { + return new ViewModelOperationResult(false, $"Unable to load Headers: {e.Message}"); + } + } - /// - /// Reads column headers from the loaded file - /// - /// Object which contains information and results of this method - public ViewModelOperationResult LoadHeaders() + /// + /// Reset all figures and parsed records + /// + private void ResetLoadedProfileData() + { + SelectedAccount = new Account(); + ParsedRecords.Clear(); + TotalRecords = 0; + RecordsWithErrors = 0; + ValidRecords = 0; + } + + /// + /// Reads the file and parses the content to a set of . + /// Results will be stored in + /// + /// + /// Sets also figures of the ViewModel like or + /// + /// Object which contains information and results of this method + public ViewModelOperationResult ValidateData() + { + try { - try + // Pre-Load Data for verification + // Initialize CsvReader + var options = new Options(SelectedImportProfile.TextQualifier, '\\', SelectedImportProfile.Delimiter); + var tokenizer = new RFC4180Tokenizer(options); + var csvParserOptions = new CsvParserOptions(true, tokenizer); + var csvReaderOptions = new CsvReaderOptions(new[] { Environment.NewLine }); + var csvMapper = new CsvBankTransactionMapping(SelectedImportProfile, IdentifiedColumns); + var csvParser = new CsvParser(csvParserOptions, csvMapper); + + // Read File and Skip rows based on HeaderRow + var stringBuilder = new StringBuilder(); + for (int i = SelectedImportProfile.HeaderRow-1; i < _fileLines.Length; i++) { - // Set ComboBox selection for Column Mapping - IdentifiedColumns.Clear(); - var headerLine = _fileLines[SelectedImportProfile.HeaderRow - 1]; - var columns = headerLine.Split(SelectedImportProfile.Delimiter); - foreach (var column in columns) - { - if (column != string.Empty) - IdentifiedColumns.Add(column.Trim(SelectedImportProfile.TextQualifier)); // Exclude TextQualifier - } - - return new ViewModelOperationResult(true); + stringBuilder.AppendLine(_fileLines[i]); } - catch (Exception e) + + // Parse Csv File + var parsedResults = csvParser.ReadFromString(csvReaderOptions, stringBuilder.ToString()).ToList(); + + ParsedRecords.Clear(); + foreach (var parsedResult in parsedResults) { - return new ViewModelOperationResult(false, $"Unable to load Headers: {e.Message}"); + ParsedRecords.Add(parsedResult); } - } - /// - /// Reset all figures and parsed records - /// - private void ResetLoadedProfileData() + TotalRecords = parsedResults.Count; + RecordsWithErrors = parsedResults.Count(i => !i.IsValid); + ValidRecords = parsedResults.Count(i => i.IsValid); + + if (ValidRecords > 0) _isProfileValid = true; + return new ViewModelOperationResult(true); + } + catch (Exception e) { - SelectedAccount = new Account(); - ParsedRecords.Clear(); TotalRecords = 0; RecordsWithErrors = 0; ValidRecords = 0; + ParsedRecords.Clear(); + return new ViewModelOperationResult(false, e.Message); } + } - /// - /// Reads the file and parses the content to a set of . - /// Results will be stored in - /// - /// - /// Sets also figures of the ViewModel like or - /// - /// Object which contains information and results of this method - public ViewModelOperationResult ValidateData() + /// + /// Uses data from to store it in the database + /// + /// + /// This method will call + /// + /// Object which contains information and results of this method + public ViewModelOperationResult ImportData() + { + if (!_isProfileValid) return new ViewModelOperationResult(false, "Unable to Import Data as current settings are invalid."); + + ValidateData(); + using (var dbContext = new DatabaseContext(_dbOptions)) { - try + using (var transaction = dbContext.Database.BeginTransaction()) { - // Pre-Load Data for verification - // Initialize CsvReader - var options = new Options(SelectedImportProfile.TextQualifier, '\\', SelectedImportProfile.Delimiter); - var tokenizer = new RFC4180Tokenizer(options); - var csvParserOptions = new CsvParserOptions(true, tokenizer); - var csvReaderOptions = new CsvReaderOptions(new[] { Environment.NewLine }); - var csvMapper = new CsvBankTransactionMapping(SelectedImportProfile, IdentifiedColumns); - var csvParser = new CsvParser(csvParserOptions, csvMapper); - - // Read File and Skip rows based on HeaderRow - var stringBuilder = new StringBuilder(); - for (int i = SelectedImportProfile.HeaderRow-1; i < _fileLines.Length; i++) + try { - stringBuilder.AppendLine(_fileLines[i]); + var importedCount = 0; + var newRecords = new List(); + foreach (var parsedRecord in ParsedRecords.Where(i => i.IsValid)) + { + var newRecord = parsedRecord.Result; + newRecord.AccountId = SelectedAccount.AccountId; + newRecords.Add(newRecord); + } + importedCount = dbContext.CreateBankTransactions(newRecords); + + transaction.Commit(); + return new ViewModelOperationResult(true, $"Successfully imported {importedCount} records."); } - - // Parse Csv File - var parsedResults = csvParser.ReadFromString(csvReaderOptions, stringBuilder.ToString()).ToList(); - - ParsedRecords.Clear(); - foreach (var parsedResult in parsedResults) + catch (Exception e) { - ParsedRecords.Add(parsedResult); + transaction.Rollback(); + return new ViewModelOperationResult(false, $"Unable to Import Data. Error message: {e.Message}"); } - - TotalRecords = parsedResults.Count; - RecordsWithErrors = parsedResults.Count(i => !i.IsValid); - ValidRecords = parsedResults.Count(i => i.IsValid); - - if (ValidRecords > 0) _isProfileValid = true; - return new ViewModelOperationResult(true); - } - catch (Exception e) - { - TotalRecords = 0; - RecordsWithErrors = 0; - ValidRecords = 0; - ParsedRecords.Clear(); - return new ViewModelOperationResult(false, e.Message); } } + } - /// - /// Uses data from to store it in the database - /// - /// - /// This method will call - /// - /// Object which contains information and results of this method - public ViewModelOperationResult ImportData() + /// + /// Helper method to load from the database + /// + private void LoadAvailableProfiles() + { + AvailableImportProfiles.Clear(); + using (var dbContext = new DatabaseContext(_dbOptions)) { - if (!_isProfileValid) return new ViewModelOperationResult(false, "Unable to Import Data as current settings are invalid."); - - ValidateData(); - using (var dbContext = new DatabaseContext(_dbOptions)) + foreach (var profile in dbContext.ImportProfile) { - using (var transaction = dbContext.Database.BeginTransaction()) - { - try - { - var importedCount = 0; - var newRecords = new List(); - foreach (var parsedRecord in ParsedRecords.Where(i => i.IsValid)) - { - var newRecord = parsedRecord.Result; - newRecord.AccountId = SelectedAccount.AccountId; - newRecords.Add(newRecord); - } - importedCount = dbContext.CreateBankTransactions(newRecords); - - transaction.Commit(); - return new ViewModelOperationResult(true, $"Successfully imported {importedCount} records."); - } - catch (Exception e) - { - transaction.Rollback(); - return new ViewModelOperationResult(false, $"Unable to Import Data. Error message: {e.Message}"); - } - } + AvailableImportProfiles.Add(profile); } } + } - /// - /// Helper method to load from the database - /// - private void LoadAvailableProfiles() + /// + /// Creates a new in the database based on data + /// + /// Object which contains information and results of this method + public ViewModelOperationResult CreateProfile() + { + try { - AvailableImportProfiles.Clear(); + if (string.IsNullOrEmpty(SelectedImportProfile.ProfileName)) throw new Exception("Profile Name must not be empty."); + using (var dbContext = new DatabaseContext(_dbOptions)) { - foreach (var profile in dbContext.ImportProfile) - { - AvailableImportProfiles.Add(profile); - } - } + SelectedImportProfile.ImportProfileId = 0; + if (dbContext.CreateImportProfile(SelectedImportProfile) == 0) + throw new Exception("Profile could not be created in database."); + LoadAvailableProfiles(); + SelectedImportProfile = AvailableImportProfiles.First(i => i.ImportProfileId == SelectedImportProfile.ImportProfileId); + } + + return new ViewModelOperationResult(true); } - - /// - /// Creates a new in the database based on data - /// - /// Object which contains information and results of this method - public ViewModelOperationResult CreateProfile() + catch (Exception e) { - try - { - if (string.IsNullOrEmpty(SelectedImportProfile.ProfileName)) throw new Exception("Profile Name must not be empty."); - - using (var dbContext = new DatabaseContext(_dbOptions)) - { - SelectedImportProfile.ImportProfileId = 0; - if (dbContext.CreateImportProfile(SelectedImportProfile) == 0) - throw new Exception("Profile could not be created in database."); - LoadAvailableProfiles(); - SelectedImportProfile = AvailableImportProfiles.First(i => i.ImportProfileId == SelectedImportProfile.ImportProfileId); - } - - return new ViewModelOperationResult(true); - } - catch (Exception e) - { - return new ViewModelOperationResult(false, $"Unable to create Import Profile: {e.Message}"); - } + return new ViewModelOperationResult(false, $"Unable to create Import Profile: {e.Message}"); } + } - /// - /// Updates data of the current in the database - /// - /// Object which contains information and results of this method - public ViewModelOperationResult SaveProfile() + /// + /// Updates data of the current in the database + /// + /// Object which contains information and results of this method + public ViewModelOperationResult SaveProfile() + { + try { - try - { - if (string.IsNullOrEmpty(SelectedImportProfile.ProfileName)) throw new Exception("Profile Name must not be empty."); + if (string.IsNullOrEmpty(SelectedImportProfile.ProfileName)) throw new Exception("Profile Name must not be empty."); - using (var dbContext = new DatabaseContext(_dbOptions)) - { - dbContext.UpdateImportProfile(SelectedImportProfile); - - LoadAvailableProfiles(); - SelectedImportProfile = AvailableImportProfiles.First(i => i.ImportProfileId == SelectedImportProfile.ImportProfileId); - } - - return new ViewModelOperationResult(true); - } - catch (Exception e) + using (var dbContext = new DatabaseContext(_dbOptions)) { - return new ViewModelOperationResult(false, $"Unable to save Import Profile: {e.Message}"); - } + dbContext.UpdateImportProfile(SelectedImportProfile); + + LoadAvailableProfiles(); + SelectedImportProfile = AvailableImportProfiles.First(i => i.ImportProfileId == SelectedImportProfile.ImportProfileId); + } + + return new ViewModelOperationResult(true); + } + catch (Exception e) + { + return new ViewModelOperationResult(false, $"Unable to save Import Profile: {e.Message}"); } + } - /// - /// Deletes the in the database based on - /// - /// Object which contains information and results of this method - public ViewModelOperationResult DeleteProfile() + /// + /// Deletes the in the database based on + /// + /// Object which contains information and results of this method + public ViewModelOperationResult DeleteProfile() + { + try { - try + using (var dbContext = new DatabaseContext(_dbOptions)) { - using (var dbContext = new DatabaseContext(_dbOptions)) - { - dbContext.DeleteImportProfile(SelectedImportProfile); - } - ResetLoadedProfileData(); - LoadAvailableProfiles(); + dbContext.DeleteImportProfile(SelectedImportProfile); + } + ResetLoadedProfileData(); + LoadAvailableProfiles(); - return new ViewModelOperationResult(true); - } - catch (Exception e) - { - return new ViewModelOperationResult(false, $"Unable to delete Import Profile: {e.Message}"); - } + return new ViewModelOperationResult(true); + } + catch (Exception e) + { + return new ViewModelOperationResult(false, $"Unable to delete Import Profile: {e.Message}"); } } } diff --git a/OpenBudgeteer.Core/ViewModels/ItemViewModels/AccountViewModelItem.cs b/OpenBudgeteer.Core/ViewModels/ItemViewModels/AccountViewModelItem.cs index 4caf34e..df10700 100644 --- a/OpenBudgeteer.Core/ViewModels/ItemViewModels/AccountViewModelItem.cs +++ b/OpenBudgeteer.Core/ViewModels/ItemViewModels/AccountViewModelItem.cs @@ -1,108 +1,103 @@ -using System; -using System.Collections.Generic; -using System.Text; -using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore; using OpenBudgeteer.Core.Common; using OpenBudgeteer.Core.Common.Database; -using OpenBudgeteer.Core.Common.EventClasses; using OpenBudgeteer.Core.Models; -namespace OpenBudgeteer.Core.ViewModels.ItemViewModels +namespace OpenBudgeteer.Core.ViewModels.ItemViewModels; + +public class AccountViewModelItem : ViewModelBase { - public class AccountViewModelItem : ViewModelBase + private Account _account; + /// + /// Reference to model object in the database + /// + public Account Account { - private Account _account; - /// - /// Reference to model object in the database - /// - public Account Account - { - get => _account; - internal set => Set(ref _account, value); - } + get => _account; + internal set => Set(ref _account, value); + } - private decimal _balance; - /// - /// Total account balance - /// - public decimal Balance - { - get => _balance; - set => Set(ref _balance, value); - } + private decimal _balance; + /// + /// Total account balance + /// + public decimal Balance + { + get => _balance; + set => Set(ref _balance, value); + } - private decimal _in; - /// - /// Total income of the account - /// - public decimal In - { - get => _in; - set => Set(ref _in, value); - } + private decimal _in; + /// + /// Total income of the account + /// + public decimal In + { + get => _in; + set => Set(ref _in, value); + } - private decimal _out; - /// - /// Total expenses of the account - /// - public decimal Out - { - get => _out; - set => Set(ref _out, value); - } + private decimal _out; + /// + /// Total expenses of the account + /// + public decimal Out + { + get => _out; + set => Set(ref _out, value); + } - private readonly DbContextOptions _dbOptions; + private readonly DbContextOptions _dbOptions; - /// - /// Basic constructor - /// - /// Options to connect to a database - public AccountViewModelItem(DbContextOptions dbOptions) - { - _dbOptions = dbOptions; - } + /// + /// Basic constructor + /// + /// Options to connect to a database + public AccountViewModelItem(DbContextOptions dbOptions) + { + _dbOptions = dbOptions; + } - /// - /// Initialize ViewModel with an existing object - /// - /// Options to connect to a database - /// Account instance - public AccountViewModelItem(DbContextOptions dbOptions, Account account) : this(dbOptions) - { - _account = account; - } + /// + /// Initialize ViewModel with an existing object + /// + /// Options to connect to a database + /// Account instance + public AccountViewModelItem(DbContextOptions dbOptions, Account account) : this(dbOptions) + { + _account = account; + } - /// - /// Creates or updates a record in the database based on object - /// - /// Triggers - /// Object which contains information and results of this method - public ViewModelOperationResult CreateUpdateAccount() + /// + /// Creates or updates a record in the database based on object + /// + /// Triggers + /// Object which contains information and results of this method + public ViewModelOperationResult CreateUpdateAccount() + { + using (var dbContext = new DatabaseContext(_dbOptions)) { - using (var dbContext = new DatabaseContext(_dbOptions)) - { - var result = Account.AccountId == 0 ? dbContext.CreateAccount(Account) : dbContext.UpdateAccount(Account); - if (result == 0) return new ViewModelOperationResult(false, "Unable to save changes to database"); - return new ViewModelOperationResult(true, true); - } + var result = Account.AccountId == 0 ? dbContext.CreateAccount(Account) : dbContext.UpdateAccount(Account); + if (result == 0) return new ViewModelOperationResult(false, "Unable to save changes to database"); + return new ViewModelOperationResult(true, true); } + } - /// - /// Sets Inactive flag for a record in the database based on object. - /// - /// Triggers - /// Object which contains information and results of this method - public ViewModelOperationResult CloseAccount() - { - if (Balance != 0) return new ViewModelOperationResult(false, "Balance must be 0 to close an Account"); + /// + /// Sets Inactive flag for a record in the database based on object. + /// + /// Triggers + /// Object which contains information and results of this method + public ViewModelOperationResult CloseAccount() + { + if (Balance != 0) return new ViewModelOperationResult(false, "Balance must be 0 to close an Account"); - Account.IsActive = 0; - using (var dbContext = new DatabaseContext(_dbOptions)) - { - var result = dbContext.UpdateAccount(Account); - if (result == 0) return new ViewModelOperationResult(false, "Unable to save changes to database"); - return new ViewModelOperationResult(true, true); - } + Account.IsActive = 0; + using (var dbContext = new DatabaseContext(_dbOptions)) + { + var result = dbContext.UpdateAccount(Account); + if (result == 0) return new ViewModelOperationResult(false, "Unable to save changes to database"); + return new ViewModelOperationResult(true, true); } } } diff --git a/OpenBudgeteer.Core/ViewModels/ItemViewModels/BucketGroupViewModelItem.cs b/OpenBudgeteer.Core/ViewModels/ItemViewModels/BucketGroupViewModelItem.cs index 030b18f..1101079 100644 --- a/OpenBudgeteer.Core/ViewModels/ItemViewModels/BucketGroupViewModelItem.cs +++ b/OpenBudgeteer.Core/ViewModels/ItemViewModels/BucketGroupViewModelItem.cs @@ -1,207 +1,203 @@ using OpenBudgeteer.Core.Common.Database; using OpenBudgeteer.Core.Models; using System; -using System.Collections.Generic; using System.Collections.ObjectModel; using System.Linq; -using System.Text; using Microsoft.EntityFrameworkCore; using OpenBudgeteer.Core.Common; -using OpenBudgeteer.Core.Common.EventClasses; -namespace OpenBudgeteer.Core.ViewModels.ItemViewModels +namespace OpenBudgeteer.Core.ViewModels.ItemViewModels; + +public class BucketGroupViewModelItem : ViewModelBase { - public class BucketGroupViewModelItem : ViewModelBase + private BucketGroup _bucketGroup; + /// + /// Reference to model object in the database + /// + public BucketGroup BucketGroup { - private BucketGroup _bucketGroup; - /// - /// Reference to model object in the database - /// - public BucketGroup BucketGroup - { - get => _bucketGroup; - internal set => Set(ref _bucketGroup, value); - } + get => _bucketGroup; + internal set => Set(ref _bucketGroup, value); + } - private decimal _totalBalance; - /// - /// Balance of all Buckets assigned to the BucketGroup - /// - public decimal TotalBalance - { - get => _totalBalance; - set => Set(ref _totalBalance, value); - } + private decimal _totalBalance; + /// + /// Balance of all Buckets assigned to the BucketGroup + /// + public decimal TotalBalance + { + get => _totalBalance; + set => Set(ref _totalBalance, value); + } - private bool _isHovered; - /// - /// Helper property to check if the cursor hovers over the entry in the UI - /// - public bool IsHovered - { - get => _isHovered; - set => Set(ref _isHovered, value); - } + private bool _isHovered; + /// + /// Helper property to check if the cursor hovers over the entry in the UI + /// + public bool IsHovered + { + get => _isHovered; + set => Set(ref _isHovered, value); + } - private bool _isCollapsed; - /// - /// Helper property to check if the list of assigned Buckets is collapsed - /// - public bool IsCollapsed - { - get => _isCollapsed; - set => Set(ref _isCollapsed, value); - } + private bool _isCollapsed; + /// + /// Helper property to check if the list of assigned Buckets is collapsed + /// + public bool IsCollapsed + { + get => _isCollapsed; + set => Set(ref _isCollapsed, value); + } - private ObservableCollection _buckets; - /// - /// Collection of Buckets assigned to this BucketGroup - /// - public ObservableCollection Buckets - { - get => _buckets; - set => Set(ref _buckets, value); - } + private ObservableCollection _buckets; + /// + /// Collection of Buckets assigned to this BucketGroup + /// + public ObservableCollection Buckets + { + get => _buckets; + set => Set(ref _buckets, value); + } - private bool _inModification; - /// - /// Helper property to check if the BucketGroup is currently modified - /// - public bool InModification - { - get => _inModification; - set => Set(ref _inModification, value); - } - - private readonly DateTime _currentMonth; - private readonly DbContextOptions _dbOptions; - private BucketGroup _oldBucketGroup; + private bool _inModification; + /// + /// Helper property to check if the BucketGroup is currently modified + /// + public bool InModification + { + get => _inModification; + set => Set(ref _inModification, value); + } + + private readonly DateTime _currentMonth; + private readonly DbContextOptions _dbOptions; + private BucketGroup _oldBucketGroup; - /// - /// Basic constructor - /// - /// Options to connect to a database - public BucketGroupViewModelItem(DbContextOptions dbOptions) - { - Buckets = new ObservableCollection(); - InModification = false; - _dbOptions = dbOptions; - } + /// + /// Basic constructor + /// + /// Options to connect to a database + public BucketGroupViewModelItem(DbContextOptions dbOptions) + { + Buckets = new ObservableCollection(); + InModification = false; + _dbOptions = dbOptions; + } - /// - /// Initialize ViewModel based on an existing object and a specific YearMonth - /// - /// Options to connect to a database - /// BucketGroup instance - /// YearMonth that should be used - public BucketGroupViewModelItem(DbContextOptions dbOptions, BucketGroup bucketGroup, DateTime currentMonth) : this(dbOptions) - { - BucketGroup = bucketGroup; - _currentMonth = currentMonth; - } + /// + /// Initialize ViewModel based on an existing object and a specific YearMonth + /// + /// Options to connect to a database + /// BucketGroup instance + /// YearMonth that should be used + public BucketGroupViewModelItem(DbContextOptions dbOptions, BucketGroup bucketGroup, DateTime currentMonth) : this(dbOptions) + { + BucketGroup = bucketGroup; + _currentMonth = currentMonth; + } - /// - /// Helper method to start modification process and creating a backup of current values - /// - public void StartModification() + /// + /// Helper method to start modification process and creating a backup of current values + /// + public void StartModification() + { + _oldBucketGroup = new BucketGroup() { - _oldBucketGroup = new BucketGroup() - { - BucketGroupId = BucketGroup.BucketGroupId, - Name = BucketGroup.Name, - Position = BucketGroup.Position - }; - InModification = true; - } + BucketGroupId = BucketGroup.BucketGroupId, + Name = BucketGroup.Name, + Position = BucketGroup.Position + }; + InModification = true; + } + + /// + /// Stops modification and restores previous values + /// + public void CancelModification() + { + BucketGroup = _oldBucketGroup; + InModification = false; + _oldBucketGroup = null; + } - /// - /// Stops modification and restores previous values - /// - public void CancelModification() + /// + /// Updates a record in the database based on object + /// + /// Triggers + /// Object which contains information and results of this method + public ViewModelOperationResult SaveModification() + { + try { - BucketGroup = _oldBucketGroup; + using (var dbContext = new DatabaseContext(_dbOptions)) + { + dbContext.UpdateBucketGroup(BucketGroup); + } InModification = false; _oldBucketGroup = null; + return new ViewModelOperationResult(true, true); } - - /// - /// Updates a record in the database based on object - /// - /// Triggers - /// Object which contains information and results of this method - public ViewModelOperationResult SaveModification() + catch (Exception e) { - try - { - using (var dbContext = new DatabaseContext(_dbOptions)) - { - dbContext.UpdateBucketGroup(BucketGroup); - } - InModification = false; - _oldBucketGroup = null; - return new ViewModelOperationResult(true, true); - } - catch (Exception e) - { - return new ViewModelOperationResult(false, $"Unable to write changes to database: {e.Message}"); - } + return new ViewModelOperationResult(false, $"Unable to write changes to database: {e.Message}"); } + } - /// - /// Moves the position of the BucketGroup according to the passed value. Updates positions for all other - /// BucketGroups accordingly - /// - /// Number of positions that BucketGroup needs to be moved - /// Triggers - /// Object which contains information and results of this method - public ViewModelOperationResult MoveGroup(int positions) + /// + /// Moves the position of the BucketGroup according to the passed value. Updates positions for all other + /// BucketGroups accordingly + /// + /// Number of positions that BucketGroup needs to be moved + /// Triggers + /// Object which contains information and results of this method + public ViewModelOperationResult MoveGroup(int positions) + { + if (positions == 0) return new ViewModelOperationResult(true); + using (var dbContext = new DatabaseContext(_dbOptions)) { - if (positions == 0) return new ViewModelOperationResult(true); - using (var dbContext = new DatabaseContext(_dbOptions)) + using (var transaction = dbContext.Database.BeginTransaction()) { - using (var transaction = dbContext.Database.BeginTransaction()) + try { - try + var bucketGroupCount = dbContext.BucketGroup.Count(); + var targetPosition = BucketGroup.Position + positions; + if (targetPosition < 1) targetPosition = 1; + if (targetPosition > bucketGroupCount) targetPosition = bucketGroupCount; + if (targetPosition == BucketGroup.Position) return new ViewModelOperationResult(true); // Group is already at the end or top. No further action + // Move Group in an interim List + var existingBucketGroups = new ObservableCollection(); + foreach (var bucketGroup in dbContext.BucketGroup.OrderBy(i => i.Position)) { - var bucketGroupCount = dbContext.BucketGroup.Count(); - var targetPosition = BucketGroup.Position + positions; - if (targetPosition < 1) targetPosition = 1; - if (targetPosition > bucketGroupCount) targetPosition = bucketGroupCount; - if (targetPosition == BucketGroup.Position) return new ViewModelOperationResult(true); // Group is already at the end or top. No further action - // Move Group in an interim List - var existingBucketGroups = new ObservableCollection(); - foreach (var bucketGroup in dbContext.BucketGroup.OrderBy(i => i.Position)) - { - existingBucketGroups.Add(bucketGroup); - } - existingBucketGroups.Move(BucketGroup.Position-1, targetPosition-1); - - // Update Position number - var newPosition = 1; - foreach (var bucketGroup in existingBucketGroups) - { - bucketGroup.Position = newPosition; - dbContext.UpdateBucketGroup(bucketGroup); - newPosition++; - } - - transaction.Commit(); - return new ViewModelOperationResult(true, true); + existingBucketGroups.Add(bucketGroup); } - catch (Exception e) + existingBucketGroups.Move(BucketGroup.Position-1, targetPosition-1); + + // Update Position number + var newPosition = 1; + foreach (var bucketGroup in existingBucketGroups) { - transaction.Rollback(); - return new ViewModelOperationResult(false, $"Unable to move Bucket Group: {e.Message}"); + bucketGroup.Position = newPosition; + dbContext.UpdateBucketGroup(bucketGroup); + newPosition++; } + + transaction.Commit(); + return new ViewModelOperationResult(true, true); + } + catch (Exception e) + { + transaction.Rollback(); + return new ViewModelOperationResult(false, $"Unable to move Bucket Group: {e.Message}"); } } } + } - public BucketViewModelItem CreateBucket() - { - var newBucket = new BucketViewModelItem(_dbOptions, BucketGroup, _currentMonth); - Buckets.Add(newBucket); - return newBucket; - } + public BucketViewModelItem CreateBucket() + { + var newBucket = new BucketViewModelItem(_dbOptions, BucketGroup, _currentMonth); + Buckets.Add(newBucket); + return newBucket; } } diff --git a/OpenBudgeteer.Core/ViewModels/ItemViewModels/BucketViewModelItem.cs b/OpenBudgeteer.Core/ViewModels/ItemViewModels/BucketViewModelItem.cs index d07f53f..a367e88 100644 --- a/OpenBudgeteer.Core/ViewModels/ItemViewModels/BucketViewModelItem.cs +++ b/OpenBudgeteer.Core/ViewModels/ItemViewModels/BucketViewModelItem.cs @@ -9,714 +9,713 @@ using System.Threading.Tasks; using OpenBudgeteer.Core.Common; -namespace OpenBudgeteer.Core.ViewModels.ItemViewModels +namespace OpenBudgeteer.Core.ViewModels.ItemViewModels; + +public class BucketViewModelItem : ViewModelBase { - public class BucketViewModelItem : ViewModelBase + private Bucket _bucket; + /// + /// Reference to model object in the database + /// + public Bucket Bucket { - private Bucket _bucket; - /// - /// Reference to model object in the database - /// - public Bucket Bucket - { - get => _bucket; - set => Set(ref _bucket, value); - } + get => _bucket; + set => Set(ref _bucket, value); + } - private BucketVersion _bucketVersion; - /// - /// Reference to model object in the database - /// - public BucketVersion BucketVersion - { - get => _bucketVersion; - set => Set(ref _bucketVersion, value); - } + private BucketVersion _bucketVersion; + /// + /// Reference to model object in the database + /// + public BucketVersion BucketVersion + { + get => _bucketVersion; + set => Set(ref _bucketVersion, value); + } - private decimal _balance; - /// - /// Overall Balance of a for the whole time - /// - public decimal Balance - { - get => _balance; - set => Set(ref _balance, value); - } + private decimal _balance; + /// + /// Overall Balance of a for the whole time + /// + public decimal Balance + { + get => _balance; + set => Set(ref _balance, value); + } - private decimal _inOut; - /// - /// This will be just the input field for Bucket movements - /// - public decimal InOut - { - get => _inOut; - set => Set(ref _inOut, value); - } + private decimal _inOut; + /// + /// This will be just the input field for Bucket movements + /// + public decimal InOut + { + get => _inOut; + set => Set(ref _inOut, value); + } - private decimal _want; - /// - /// Shows how many money a want to have for a specific month - /// - public decimal Want - { - get => _want; - set => Set(ref _want, value); - } + private decimal _want; + /// + /// Shows how many money a want to have for a specific month + /// + public decimal Want + { + get => _want; + set => Set(ref _want, value); + } - private decimal _in; - /// - /// Sum of all movements - /// - public decimal In - { - get => _in; - set => Set(ref _in, value); - } + private decimal _in; + /// + /// Sum of all movements + /// + public decimal In + { + get => _in; + set => Set(ref _in, value); + } - private decimal _activity; - /// - /// Sum of money for all in a specific month - /// - public decimal Activity - { - get => _activity; - set => Set(ref _activity, value); - } + private decimal _activity; + /// + /// Sum of money for all in a specific month + /// + public decimal Activity + { + get => _activity; + set => Set(ref _activity, value); + } - private string _details; - /// - /// Contains information of the progress for with 3 and 4 - /// - public string Details - { - get => _details; - set => Set(ref _details, value); - } + private string _details; + /// + /// Contains information of the progress for with 3 and 4 + /// + public string Details + { + get => _details; + set => Set(ref _details, value); + } - private int _progress; - /// - /// Contains the progress in % - /// - public int Progress - { - get => _progress; - set => Set(ref _progress, value); - } + private int _progress; + /// + /// Contains the progress in % + /// + public int Progress + { + get => _progress; + set => Set(ref _progress, value); + } - private bool _isProgressBarVisible; - /// - /// Helper property to set the visibility of the ProgressBar if 3 or 4 - /// - public bool IsProgressbarVisible - { - get => _isProgressBarVisible; - set => Set(ref _isProgressBarVisible, value); - } + private bool _isProgressBarVisible; + /// + /// Helper property to set the visibility of the ProgressBar if 3 or 4 + /// + public bool IsProgressbarVisible + { + get => _isProgressBarVisible; + set => Set(ref _isProgressBarVisible, value); + } - private bool _isHovered; - /// - /// Helper property to check if the cursor hovers over the entry in the UI - /// - public bool IsHovered - { - get => _isHovered; - set => Set(ref _isHovered, value); - } + private bool _isHovered; + /// + /// Helper property to check if the cursor hovers over the entry in the UI + /// + public bool IsHovered + { + get => _isHovered; + set => Set(ref _isHovered, value); + } - private bool _inModification; - /// - /// Helper property to check if the Bucket is currently modified - /// - public bool InModification - { - get => _inModification; - set => Set(ref _inModification, value); - } + private bool _inModification; + /// + /// Helper property to check if the Bucket is currently modified + /// + public bool InModification + { + get => _inModification; + set => Set(ref _inModification, value); + } - private ObservableCollection _availableBucketTypes; - /// - /// Helper collection to list BucketTypes explanations - /// - public ObservableCollection AvailableBucketTypes - { - get => _availableBucketTypes; - set => Set(ref _availableBucketTypes, value); - } + private ObservableCollection _availableBucketTypes; + /// + /// Helper collection to list BucketTypes explanations + /// + public ObservableCollection AvailableBucketTypes + { + get => _availableBucketTypes; + set => Set(ref _availableBucketTypes, value); + } - private ObservableCollection _availableColors; - /// - /// Helper collection to list available System colors - /// - public ObservableCollection AvailableColors - { - get => _availableColors; - set => Set(ref _availableColors, value); - } + private ObservableCollection _availableColors; + /// + /// Helper collection to list available System colors + /// + public ObservableCollection AvailableColors + { + get => _availableColors; + set => Set(ref _availableColors, value); + } - private ObservableCollection _availableBucketGroups; - /// - /// Helper collection to list available where this Bucket can be assigned to - /// - public ObservableCollection AvailableBucketGroups - { - get => _availableBucketGroups; - set => Set(ref _availableBucketGroups, value); - } + private ObservableCollection _availableBucketGroups; + /// + /// Helper collection to list available where this Bucket can be assigned to + /// + public ObservableCollection AvailableBucketGroups + { + get => _availableBucketGroups; + set => Set(ref _availableBucketGroups, value); + } - private readonly bool _isNewlyCreatedBucket; - private readonly DateTime _currentYearMonth; - private readonly DbContextOptions _dbOptions; + private readonly bool _isNewlyCreatedBucket; + private readonly DateTime _currentYearMonth; + private readonly DbContextOptions _dbOptions; - /// - /// Basic constructor - /// - /// Options to connect to a database - public BucketViewModelItem(DbContextOptions dbOptions) + /// + /// Basic constructor + /// + /// Options to connect to a database + public BucketViewModelItem(DbContextOptions dbOptions) + { + _dbOptions = dbOptions; + AvailableBucketGroups = new ObservableCollection(); + using (var dbContext = new DatabaseContext(_dbOptions)) { - _dbOptions = dbOptions; - AvailableBucketGroups = new ObservableCollection(); - using (var dbContext = new DatabaseContext(_dbOptions)) + foreach (var item in dbContext.BucketGroup) { - foreach (var item in dbContext.BucketGroup) - { - AvailableBucketGroups.Add(item); - } + AvailableBucketGroups.Add(item); } - AvailableBucketTypes = new ObservableCollection() - { - "Standard Bucket", - "Monthly expense", - "Expense every X Months", - "Save X until Y date" - }; - GetKnownColors(); - InModification = false; - - void GetKnownColors() + } + AvailableBucketTypes = new ObservableCollection() + { + "Standard Bucket", + "Monthly expense", + "Expense every X Months", + "Save X until Y date" + }; + GetKnownColors(); + InModification = false; + + void GetKnownColors() + { + AvailableColors = new ObservableCollection(); + var colorType = typeof(Color); + var propInfos = colorType.GetProperties(BindingFlags.Static | BindingFlags.DeclaredOnly | BindingFlags.Public); + foreach (var propInfo in propInfos) { - AvailableColors = new ObservableCollection(); - var colorType = typeof(Color); - var propInfos = colorType.GetProperties(BindingFlags.Static | BindingFlags.DeclaredOnly | BindingFlags.Public); - foreach (var propInfo in propInfos) - { - AvailableColors.Add(Color.FromName(propInfo.Name)); - } + AvailableColors.Add(Color.FromName(propInfo.Name)); } } + } - /// - /// Initialize ViewModel based on a specific YearMonth - /// - /// Creates an initial - /// Options to connect to a database - /// YearMonth that should be used - public BucketViewModelItem(DbContextOptions dbOptions, DateTime yearMonth) : this(dbOptions) + /// + /// Initialize ViewModel based on a specific YearMonth + /// + /// Creates an initial + /// Options to connect to a database + /// YearMonth that should be used + public BucketViewModelItem(DbContextOptions dbOptions, DateTime yearMonth) : this(dbOptions) + { + _currentYearMonth = new DateTime(yearMonth.Year, yearMonth.Month, 1); + BucketVersion = new BucketVersion() { - _currentYearMonth = new DateTime(yearMonth.Year, yearMonth.Month, 1); - BucketVersion = new BucketVersion() - { - BucketId = 0, - BucketType = 1, - BucketTypeZParam = yearMonth, - ValidFrom = yearMonth - }; - } + BucketId = 0, + BucketType = 1, + BucketTypeZParam = yearMonth, + ValidFrom = yearMonth + }; + } - /// - /// Initialize ViewModel based on an existing object and a specific YearMonth - /// - /// Creates an initial in active modification mode - /// Creates an initial - /// Options to connect to a database - /// BucketGroup instance - /// YearMonth that should be used - public BucketViewModelItem(DbContextOptions dbOptions, BucketGroup bucketGroup, DateTime yearMonth) : this(dbOptions, yearMonth) + /// + /// Initialize ViewModel based on an existing object and a specific YearMonth + /// + /// Creates an initial in active modification mode + /// Creates an initial + /// Options to connect to a database + /// BucketGroup instance + /// YearMonth that should be used + public BucketViewModelItem(DbContextOptions dbOptions, BucketGroup bucketGroup, DateTime yearMonth) : this(dbOptions, yearMonth) + { + _isNewlyCreatedBucket = true; + InModification = true; + Bucket = new Bucket() { - _isNewlyCreatedBucket = true; - InModification = true; - Bucket = new Bucket() - { - BucketId = 0, - BucketGroupId = bucketGroup.BucketGroupId, - Name = "New Bucket", - ValidFrom = yearMonth, - IsInactive = false, - IsInactiveFrom = DateTime.MaxValue - }; - } + BucketId = 0, + BucketGroupId = bucketGroup.BucketGroupId, + Name = "New Bucket", + ValidFrom = yearMonth, + IsInactive = false, + IsInactiveFrom = DateTime.MaxValue + }; + } + + /// + /// Initialize ViewModel based on an existing object and a specific YearMonth + /// + /// Runs to get latest + /// Options to connect to a database + /// Bucket instance + /// YearMonth that should be used + public BucketViewModelItem(DbContextOptions dbOptions, Bucket bucket, DateTime yearMonth) : this(dbOptions, yearMonth) + { + Bucket = bucket; + CalculateValues(); + } + + /// + /// Creates and returns a new ViewModel based on an existing object and a specific YearMonth + /// + /// Options to connect to a database + /// Bucket instance + /// YearMonth that should be used + /// New ViewModel instance + public static async Task CreateAsync(DbContextOptions dbOptions, Bucket bucket, DateTime yearMonth) + { + return await Task.Run(() => new BucketViewModelItem(dbOptions, bucket, yearMonth)); + } - /// - /// Initialize ViewModel based on an existing object and a specific YearMonth - /// - /// Runs to get latest - /// Options to connect to a database - /// Bucket instance - /// YearMonth that should be used - public BucketViewModelItem(DbContextOptions dbOptions, Bucket bucket, DateTime yearMonth) : this(dbOptions, yearMonth) + /// + /// Identifies latest based on and calculates all figures + /// + private void CalculateValues() + { + Balance = 0; + In = 0; + Activity = 0; + Want = 0; + InOut = 0; + + // Get latest BucketVersion based on passed parameter + using (var dbContext = new DatabaseContext(_dbOptions)) { - Bucket = bucket; - CalculateValues(); + var bucketVersions = dbContext.BucketVersion + .Where(i => i.BucketId == Bucket.BucketId) + .OrderByDescending(i => i.ValidFrom) + .ToList(); + //var orderedBucketVersions = bucketVersions.OrderByDescending(i => i.ValidFrom); + foreach (var bucketVersion in bucketVersions) + { + if (bucketVersion.ValidFrom > _currentYearMonth) continue; + BucketVersion = bucketVersion; + break; + } + if (BucketVersion == null) throw new Exception("No Bucket Version found for the selected month"); } + + #region Balance - /// - /// Creates and returns a new ViewModel based on an existing object and a specific YearMonth - /// - /// Options to connect to a database - /// Bucket instance - /// YearMonth that should be used - /// New ViewModel instance - public static async Task CreateAsync(DbContextOptions dbOptions, Bucket bucket, DateTime yearMonth) + // Get all Transactions for this Bucket until passed yearMonth + using (var dbContext = new DatabaseContext(_dbOptions)) { - return await Task.Run(() => new BucketViewModelItem(dbOptions, bucket, yearMonth)); + Balance += dbContext.BudgetedTransaction + .Join(dbContext.BankTransaction, + i => i.TransactionId, + j => j.TransactionId, + ((budgetedTransaction, bankTransaction) => new { budgetedTransaction, bankTransaction })) + .Where(i => i.budgetedTransaction.BucketId == Bucket.BucketId && + i.bankTransaction.TransactionDate < _currentYearMonth.AddMonths(1)) + .Select(i => i.budgetedTransaction) + .ToList() + .Sum(i => i.Amount); + + Balance += dbContext.BucketMovement + .Where(i => i.BucketId == Bucket.BucketId && + i.MovementDate < _currentYearMonth.AddMonths(1)) + .ToList() + .Sum(i => i.Amount); } - /// - /// Identifies latest based on and calculates all figures - /// - private void CalculateValues() + #endregion + + #region In & Activity + + using (var dbContext = new DatabaseContext(_dbOptions)) { - Balance = 0; - In = 0; - Activity = 0; - Want = 0; - InOut = 0; + var bucketTransactionsCurrentMonth = dbContext.BudgetedTransaction + .Join(dbContext.BankTransaction, + i => i.TransactionId, + j => j.TransactionId, + ((budgetedTransaction, bankTransaction) => new {budgetedTransaction, bankTransaction})) + .Where(i => i.budgetedTransaction.BucketId == Bucket.BucketId && + i.bankTransaction.TransactionDate.Year == _currentYearMonth.Year && + i.bankTransaction.TransactionDate.Month == _currentYearMonth.Month) + .Select(i => i.budgetedTransaction) + .ToList(); - // Get latest BucketVersion based on passed parameter - using (var dbContext = new DatabaseContext(_dbOptions)) + foreach (var bucketTransaction in bucketTransactionsCurrentMonth) { - var bucketVersions = dbContext.BucketVersion - .Where(i => i.BucketId == Bucket.BucketId) - .OrderByDescending(i => i.ValidFrom) - .ToList(); - //var orderedBucketVersions = bucketVersions.OrderByDescending(i => i.ValidFrom); - foreach (var bucketVersion in bucketVersions) - { - if (bucketVersion.ValidFrom > _currentYearMonth) continue; - BucketVersion = bucketVersion; - break; - } - if (BucketVersion == null) throw new Exception("No Bucket Version found for the selected month"); + if (bucketTransaction.Amount < 0) + Activity += bucketTransaction.Amount; + else + In += bucketTransaction.Amount; } - - #region Balance - // Get all Transactions for this Bucket until passed yearMonth - using (var dbContext = new DatabaseContext(_dbOptions)) + var bucketMovementsCurrentMonth = dbContext.BucketMovement + .Where(i => i.BucketId == Bucket.BucketId && + i.MovementDate.Year == _currentYearMonth.Year && + i.MovementDate.Month == _currentYearMonth.Month) + .ToList(); + + foreach (var bucketMovement in bucketMovementsCurrentMonth) { - Balance += dbContext.BudgetedTransaction - .Join(dbContext.BankTransaction, - i => i.TransactionId, - j => j.TransactionId, - ((budgetedTransaction, bankTransaction) => new { budgetedTransaction, bankTransaction })) - .Where(i => i.budgetedTransaction.BucketId == Bucket.BucketId && - i.bankTransaction.TransactionDate < _currentYearMonth.AddMonths(1)) - .Select(i => i.budgetedTransaction) - .ToList() - .Sum(i => i.Amount); - - Balance += dbContext.BucketMovement - .Where(i => i.BucketId == Bucket.BucketId && - i.MovementDate < _currentYearMonth.AddMonths(1)) - .ToList() - .Sum(i => i.Amount); + if (bucketMovement.Amount < 0) + Activity += bucketMovement.Amount; + else + In += bucketMovement.Amount; } + } - #endregion + #endregion - #region In & Activity + #region Want - using (var dbContext = new DatabaseContext(_dbOptions)) - { - var bucketTransactionsCurrentMonth = dbContext.BudgetedTransaction - .Join(dbContext.BankTransaction, - i => i.TransactionId, - j => j.TransactionId, - ((budgetedTransaction, bankTransaction) => new {budgetedTransaction, bankTransaction})) - .Where(i => i.budgetedTransaction.BucketId == Bucket.BucketId && - i.bankTransaction.TransactionDate.Year == _currentYearMonth.Year && - i.bankTransaction.TransactionDate.Month == _currentYearMonth.Month) - .Select(i => i.budgetedTransaction) - .ToList(); - - foreach (var bucketTransaction in bucketTransactionsCurrentMonth) - { - if (bucketTransaction.Amount < 0) - Activity += bucketTransaction.Amount; - else - In += bucketTransaction.Amount; - } - - var bucketMovementsCurrentMonth = dbContext.BucketMovement - .Where(i => i.BucketId == Bucket.BucketId && - i.MovementDate.Year == _currentYearMonth.Year && - i.MovementDate.Month == _currentYearMonth.Month) - .ToList(); - - foreach (var bucketMovement in bucketMovementsCurrentMonth) + switch (BucketVersion.BucketType) + { + case 2: + var newWant = BucketVersion.BucketTypeYParam - In; + Want = newWant < 0 ? 0 : newWant; + break; + case 3: + var nextTargetDate = BucketVersion.BucketTypeZParam; + while (nextTargetDate < _currentYearMonth) { - if (bucketMovement.Amount < 0) - Activity += bucketMovement.Amount; - else - In += bucketMovement.Amount; + nextTargetDate = nextTargetDate.AddMonths(BucketVersion.BucketTypeXParam); } - } + Want = CalculateWant(nextTargetDate); + break; + case 4: + Want = CalculateWant(BucketVersion.BucketTypeZParam); + break; + default: + break; + } - #endregion + decimal CalculateWant(DateTime targetDate) + { + var remainingMonths = ((targetDate.Year - _currentYearMonth.Year) * 12) + targetDate.Month - _currentYearMonth.Month; + if (remainingMonths < 0) return Balance < 0 ? Balance : 0; + if (remainingMonths == 0 && Balance < 0) return Balance * -1; + var wantForThisMonth = Math.Round((BucketVersion.BucketTypeYParam - Balance + In) / (remainingMonths + 1), 2) - In; + if (remainingMonths == 0) wantForThisMonth += Activity; // check if target amount has been consumed. Not further Want required + return wantForThisMonth < 0 ? 0 : wantForThisMonth; + } - #region Want + #endregion - switch (BucketVersion.BucketType) - { - case 2: - var newWant = BucketVersion.BucketTypeYParam - In; - Want = newWant < 0 ? 0 : newWant; - break; - case 3: - var nextTargetDate = BucketVersion.BucketTypeZParam; - while (nextTargetDate < _currentYearMonth) - { - nextTargetDate = nextTargetDate.AddMonths(BucketVersion.BucketTypeXParam); - } - Want = CalculateWant(nextTargetDate); - break; - case 4: - Want = CalculateWant(BucketVersion.BucketTypeZParam); - break; - default: - break; - } + #region Details - decimal CalculateWant(DateTime targetDate) + if (BucketVersion.BucketType is 3 or 4) + { + var targetDate = BucketVersion.BucketTypeZParam; + // Calculate new target date for BucketType 3 (Expense every X Months) + // if the selected yearMonth is already in the future + if (BucketVersion.BucketType == 3 && BucketVersion.BucketTypeZParam < _currentYearMonth) { - var remainingMonths = ((targetDate.Year - _currentYearMonth.Year) * 12) + targetDate.Month - _currentYearMonth.Month; - if (remainingMonths < 0) return Balance < 0 ? Balance : 0; - if (remainingMonths == 0 && Balance < 0) return Balance * -1; - var wantForThisMonth = Math.Round((BucketVersion.BucketTypeYParam - Balance + In) / (remainingMonths + 1), 2) - In; - if (remainingMonths == 0) wantForThisMonth += Activity; // check if target amount has been consumed. Not further Want required - return wantForThisMonth < 0 ? 0 : wantForThisMonth; + do + { + targetDate = targetDate.AddMonths(BucketVersion.BucketTypeXParam); + } while (targetDate < _currentYearMonth); } - - #endregion - - #region Details - - if (BucketVersion.BucketType is 3 or 4) + + // Special Progress handling in target month with available activity, otherwise usual calculation + if (_currentYearMonth.Month == targetDate.Month && + _currentYearMonth.Year == targetDate.Year && + Activity < 0) { - var targetDate = BucketVersion.BucketTypeZParam; - // Calculate new target date for BucketType 3 (Expense every X Months) - // if the selected yearMonth is already in the future - if (BucketVersion.BucketType == 3 && BucketVersion.BucketTypeZParam < _currentYearMonth) - { - do - { - targetDate = targetDate.AddMonths(BucketVersion.BucketTypeXParam); - } while (targetDate < _currentYearMonth); - } - - // Special Progress handling in target month with available activity, otherwise usual calculation - if (_currentYearMonth.Month == targetDate.Month && - _currentYearMonth.Year == targetDate.Year && - Activity < 0) - { - Progress = Balance >= 0 ? - // Expense as expected or lower, hence target reached and Progress 100 - 100 : - // Expense in target month was higher than expected, hence negative Balance. - // Progress based on Want and Activity - Convert.ToInt32(100 - (Want / Activity * -1) * 100); + Progress = Balance >= 0 ? + // Expense as expected or lower, hence target reached and Progress 100 + 100 : + // Expense in target month was higher than expected, hence negative Balance. + // Progress based on Want and Activity + Convert.ToInt32(100 - (Want / Activity * -1) * 100); - } - else - { - Progress = Convert.ToInt32((Balance / BucketVersion.BucketTypeYParam) * 100); - if (Progress > 100) Progress = 100; - } - - Details = $"{BucketVersion.BucketTypeYParam} until {targetDate:yyyy-MM}"; - IsProgressbarVisible = true; } else { - Progress = 0; - Details = string.Empty; - IsProgressbarVisible = false; + Progress = Convert.ToInt32((Balance / BucketVersion.BucketTypeYParam) * 100); + if (Progress > 100) Progress = 100; } - - #endregion + + Details = $"{BucketVersion.BucketTypeYParam} until {targetDate:yyyy-MM}"; + IsProgressbarVisible = true; } - - /// - /// Activates modification mode - /// - public void EditBucket() + else { - InModification = true; + Progress = 0; + Details = string.Empty; + IsProgressbarVisible = false; } - /// - /// Updates a record in the database based on object to set it as inactive. In case there - /// are no nor assigned to it, it will be deleted - /// completely from the database (including ) - /// - /// Bucket will be set to inactive for the next month - /// Triggers - /// Object which contains information and results of this method - public ViewModelOperationResult CloseBucket() + #endregion + } + + /// + /// Activates modification mode + /// + public void EditBucket() + { + InModification = true; + } + + /// + /// Updates a record in the database based on object to set it as inactive. In case there + /// are no nor assigned to it, it will be deleted + /// completely from the database (including ) + /// + /// Bucket will be set to inactive for the next month + /// Triggers + /// Object which contains information and results of this method + public ViewModelOperationResult CloseBucket() + { + if (Bucket.IsInactive) return new ViewModelOperationResult(false, "Bucket has been already set to inactive"); + if (Balance != 0) return new ViewModelOperationResult(false, "Balance must be 0 to close a Bucket"); + + using (var dbContext = new DatabaseContext(_dbOptions)) { - if (Bucket.IsInactive) return new ViewModelOperationResult(false, "Bucket has been already set to inactive"); - if (Balance != 0) return new ViewModelOperationResult(false, "Balance must be 0 to close a Bucket"); - - using (var dbContext = new DatabaseContext(_dbOptions)) + using (var transaction = dbContext.Database.BeginTransaction()) { - using (var transaction = dbContext.Database.BeginTransaction()) + try { - try + if (dbContext.BudgetedTransaction.Any(i => i.BucketId == Bucket.BucketId) || + dbContext.BucketMovement.Any(i => i.BucketId == Bucket.BucketId)) { - if (dbContext.BudgetedTransaction.Any(i => i.BucketId == Bucket.BucketId) || - dbContext.BucketMovement.Any(i => i.BucketId == Bucket.BucketId)) - { - // Bucket will be set to inactive for the next month - Bucket.IsInactive = true; - Bucket.IsInactiveFrom = _currentYearMonth.AddMonths(1); - if (dbContext.UpdateBucket(Bucket) == 0) - throw new Exception($"Unable to deactivate Bucket for next month.{Environment.NewLine}" + - $"{Environment.NewLine}" + - $"Bucket ID: {Bucket.BucketId}{Environment.NewLine}" + - $"Bucket Target Inactive Date: {Bucket.IsInactiveFrom.ToShortDateString()}"); - } - else + // Bucket will be set to inactive for the next month + Bucket.IsInactive = true; + Bucket.IsInactiveFrom = _currentYearMonth.AddMonths(1); + if (dbContext.UpdateBucket(Bucket) == 0) + throw new Exception($"Unable to deactivate Bucket for next month.{Environment.NewLine}" + + $"{Environment.NewLine}" + + $"Bucket ID: {Bucket.BucketId}{Environment.NewLine}" + + $"Bucket Target Inactive Date: {Bucket.IsInactiveFrom.ToShortDateString()}"); + } + else + { + // Bucket has no transactions & movements, so it can be directly deleted from the database + if (dbContext.DeleteBucket(Bucket) == 0) + throw new Exception($"Unable to delete Bucket.{Environment.NewLine}" + + $"{Environment.NewLine}" + + $"Bucket ID: {Bucket.BucketId}{Environment.NewLine}"); + var bucketVersions = dbContext.BucketVersion + .Where(i => i.BucketId == Bucket.BucketId) + .ToList(); + foreach (var bucketVersion in bucketVersions) { - // Bucket has no transactions & movements, so it can be directly deleted from the database - if (dbContext.DeleteBucket(Bucket) == 0) - throw new Exception($"Unable to delete Bucket.{Environment.NewLine}" + + if (dbContext.DeleteBucketVersion(bucketVersion) == 0) + throw new Exception($"Unable to delete a Bucket Version.{Environment.NewLine}" + $"{Environment.NewLine}" + - $"Bucket ID: {Bucket.BucketId}{Environment.NewLine}"); - var bucketVersions = dbContext.BucketVersion - .Where(i => i.BucketId == Bucket.BucketId) - .ToList(); - foreach (var bucketVersion in bucketVersions) - { - if (dbContext.DeleteBucketVersion(bucketVersion) == 0) - throw new Exception($"Unable to delete a Bucket Version.{Environment.NewLine}" + - $"{Environment.NewLine}" + - $"Bucket Version ID: {bucketVersion.BucketVersionId}{Environment.NewLine}" + - $"Bucket Version: {bucketVersion.Version}"); - } + $"Bucket Version ID: {bucketVersion.BucketVersionId}{Environment.NewLine}" + + $"Bucket Version: {bucketVersion.Version}"); } - transaction.Commit(); - } - catch (Exception e) - { - transaction.Rollback(); - return new ViewModelOperationResult(false, $"Error during database update: {e.Message}"); } + transaction.Commit(); } - } - return new ViewModelOperationResult(true, true); - } + catch (Exception e) + { + transaction.Rollback(); + return new ViewModelOperationResult(false, $"Error during database update: {e.Message}"); + } + } + } + return new ViewModelOperationResult(true, true); + } - /// - /// Creates or updates a record in the database based on object - /// - /// Creates also a new record in the database - /// - /// Recalculates figures after database operations in case has not been triggered - /// - /// Can trigger - /// Object which contains information and results of this method - public ViewModelOperationResult CreateOrUpdateBucket() - { - var validationResult = ValidateData(); - if (!validationResult.IsSuccessful) return validationResult; - var writeDataResult = _isNewlyCreatedBucket ? CreateBucket() : UpdateBucket(); - if (!writeDataResult.IsSuccessful || writeDataResult.ViewModelReloadRequired) return writeDataResult; - InModification = false; - CalculateValues(); - return new ViewModelOperationResult(true); - } + /// + /// Creates or updates a record in the database based on object + /// + /// Creates also a new record in the database + /// + /// Recalculates figures after database operations in case has not been triggered + /// + /// Can trigger + /// Object which contains information and results of this method + public ViewModelOperationResult CreateOrUpdateBucket() + { + var validationResult = ValidateData(); + if (!validationResult.IsSuccessful) return validationResult; + var writeDataResult = _isNewlyCreatedBucket ? CreateBucket() : UpdateBucket(); + if (!writeDataResult.IsSuccessful || writeDataResult.ViewModelReloadRequired) return writeDataResult; + InModification = false; + CalculateValues(); + return new ViewModelOperationResult(true); + } - /// - /// Runs several validation rules to prevent unintended behavior - /// - /// Object which contains information and results of this method - private ViewModelOperationResult ValidateData() + /// + /// Runs several validation rules to prevent unintended behavior + /// + /// Object which contains information and results of this method + private ViewModelOperationResult ValidateData() + { + try { - try + // Check if target amount is positive + if (BucketVersion.BucketTypeYParam < 0) { - // Check if target amount is positive - if (BucketVersion.BucketTypeYParam < 0) - { - throw new Exception("Target amount must be positive"); - } - - // Check if target amount is 0 to prevent DivideByZeroException - if ((BucketVersion.BucketType is 3 or 4) && BucketVersion.BucketTypeYParam == 0) - { - throw new Exception("Target amount must not be 0 for this Bucket Type."); - } + throw new Exception("Target amount must be positive"); } - catch (Exception e) + + // Check if target amount is 0 to prevent DivideByZeroException + if ((BucketVersion.BucketType is 3 or 4) && BucketVersion.BucketTypeYParam == 0) { - return new ViewModelOperationResult(false, e.Message); + throw new Exception("Target amount must not be 0 for this Bucket Type."); } - - return new ViewModelOperationResult(true); } + catch (Exception e) + { + return new ViewModelOperationResult(false, e.Message); + } + + return new ViewModelOperationResult(true); + } - /// - /// Creates a new record in the database based on object - /// - /// Creates also a new record in the database - /// Triggers - /// Object which contains information and results of this method - private ViewModelOperationResult CreateBucket() + /// + /// Creates a new record in the database based on object + /// + /// Creates also a new record in the database + /// Triggers + /// Object which contains information and results of this method + private ViewModelOperationResult CreateBucket() + { + using (var dbContext = new DatabaseContext(_dbOptions)) { - using (var dbContext = new DatabaseContext(_dbOptions)) + using (var transaction = dbContext.Database.BeginTransaction()) { - using (var transaction = dbContext.Database.BeginTransaction()) + try { - try - { - if (dbContext.CreateBucket(Bucket) == 0) - throw new Exception("Unable to create new Bucket."); - - var newBucketVersion = BucketVersion; - newBucketVersion.BucketId = Bucket.BucketId; - newBucketVersion.Version = 1; - newBucketVersion.ValidFrom = _currentYearMonth; - if (dbContext.CreateBucketVersion(newBucketVersion) == 0) - throw new Exception($"Unable to create new Bucket Version.{Environment.NewLine}" + - $"{Environment.NewLine}" + - $"Bucket ID: {newBucketVersion.BucketId}"); + if (dbContext.CreateBucket(Bucket) == 0) + throw new Exception("Unable to create new Bucket."); + + var newBucketVersion = BucketVersion; + newBucketVersion.BucketId = Bucket.BucketId; + newBucketVersion.Version = 1; + newBucketVersion.ValidFrom = _currentYearMonth; + if (dbContext.CreateBucketVersion(newBucketVersion) == 0) + throw new Exception($"Unable to create new Bucket Version.{Environment.NewLine}" + + $"{Environment.NewLine}" + + $"Bucket ID: {newBucketVersion.BucketId}"); - transaction.Commit(); - return new ViewModelOperationResult(true, true); - } - catch (Exception e) - { - transaction.Rollback(); - return new ViewModelOperationResult( - false, - $"Error during database update: {e.Message}", - true); - } + transaction.Commit(); + return new ViewModelOperationResult(true, true); + } + catch (Exception e) + { + transaction.Rollback(); + return new ViewModelOperationResult( + false, + $"Error during database update: {e.Message}", + true); } } } + } - /// - /// Updates a record in the database based on object - /// - /// Creates also a new record in the database - /// Can trigger - /// Object which contains information and results of this method - private ViewModelOperationResult UpdateBucket() + /// + /// Updates a record in the database based on object + /// + /// Creates also a new record in the database + /// Can trigger + /// Object which contains information and results of this method + private ViewModelOperationResult UpdateBucket() + { + using (var dbContext = new DatabaseContext(_dbOptions)) { - using (var dbContext = new DatabaseContext(_dbOptions)) + using (var transaction = dbContext.Database.BeginTransaction()) { - using (var transaction = dbContext.Database.BeginTransaction()) + try { - try + // Check on Bucket changes and update database + var dbBucket = dbContext.Bucket.First(i => i.BucketId == Bucket.BucketId); + if (dbBucket.Name != Bucket.Name || + dbBucket.ColorCode != Bucket.ColorCode || + dbBucket.BucketGroupId != Bucket.BucketGroupId) { - // Check on Bucket changes and update database - var dbBucket = dbContext.Bucket.First(i => i.BucketId == Bucket.BucketId); - if (dbBucket.Name != Bucket.Name || - dbBucket.ColorCode != Bucket.ColorCode || - dbBucket.BucketGroupId != Bucket.BucketGroupId) - { - // BucketGroup update requires special handling as ViewModel needs to trigger reload - // to force re-rendering of Blazor Page - //if (dbBucket.BucketGroupId != Bucket.BucketGroupId) forceViewModelReload = true; + // BucketGroup update requires special handling as ViewModel needs to trigger reload + // to force re-rendering of Blazor Page + //if (dbBucket.BucketGroupId != Bucket.BucketGroupId) forceViewModelReload = true; + + if (dbContext.UpdateBucket(Bucket) == 0) + throw new Exception($"Error during database update: Unable to update Bucket.{Environment.NewLine}" + + $"{Environment.NewLine}" + + $"Bucket ID: {Bucket.BucketId}"); + } - if (dbContext.UpdateBucket(Bucket) == 0) - throw new Exception($"Error during database update: Unable to update Bucket.{Environment.NewLine}" + + // Check on BucketVersion changes and create new BucketVersion + var dbBucketVersion = + dbContext.BucketVersion.First(i => i.BucketVersionId == BucketVersion.BucketVersionId); + if (dbBucketVersion.BucketType != BucketVersion.BucketType || + dbBucketVersion.BucketTypeXParam != BucketVersion.BucketTypeXParam || + dbBucketVersion.BucketTypeYParam != BucketVersion.BucketTypeYParam || + dbBucketVersion.BucketTypeZParam != BucketVersion.BucketTypeZParam || + dbBucketVersion.Notes != BucketVersion.Notes) + { + if (dbContext.BucketVersion.Any(i => + i.BucketId == BucketVersion.BucketId && i.Version > BucketVersion.Version)) + throw new Exception("Cannot create new Version as already a newer Version exists"); + + if (BucketVersion.ValidFrom == _currentYearMonth) + { + // Bucket Version modified in the same month, + // so just update the version instead of creating a new version + if (dbContext.UpdateBucketVersion(BucketVersion) == 0) + throw new Exception($"Unable to update Bucket Version.{Environment.NewLine}" + $"{Environment.NewLine}" + - $"Bucket ID: {Bucket.BucketId}"); + $"Bucket Version ID: {BucketVersion.BucketVersionId}" + + $"Bucket ID: {BucketVersion.BucketId}" + + $"Bucket Version: {BucketVersion.Version}" + + $"Bucket Version Start Date: {BucketVersion.ValidFrom.ToShortDateString()}"); } - - // Check on BucketVersion changes and create new BucketVersion - var dbBucketVersion = - dbContext.BucketVersion.First(i => i.BucketVersionId == BucketVersion.BucketVersionId); - if (dbBucketVersion.BucketType != BucketVersion.BucketType || - dbBucketVersion.BucketTypeXParam != BucketVersion.BucketTypeXParam || - dbBucketVersion.BucketTypeYParam != BucketVersion.BucketTypeYParam || - dbBucketVersion.BucketTypeZParam != BucketVersion.BucketTypeZParam || - dbBucketVersion.Notes != BucketVersion.Notes) + else { - if (dbContext.BucketVersion.Any(i => - i.BucketId == BucketVersion.BucketId && i.Version > BucketVersion.Version)) - throw new Exception("Cannot create new Version as already a newer Version exists"); - - if (BucketVersion.ValidFrom == _currentYearMonth) - { - // Bucket Version modified in the same month, - // so just update the version instead of creating a new version - if (dbContext.UpdateBucketVersion(BucketVersion) == 0) - throw new Exception($"Unable to update Bucket Version.{Environment.NewLine}" + - $"{Environment.NewLine}" + - $"Bucket Version ID: {BucketVersion.BucketVersionId}" + - $"Bucket ID: {BucketVersion.BucketId}" + - $"Bucket Version: {BucketVersion.Version}" + - $"Bucket Version Start Date: {BucketVersion.ValidFrom.ToShortDateString()}"); - } - else - { - BucketVersion.Version++; - BucketVersion.BucketVersionId = 0; - BucketVersion.ValidFrom = _currentYearMonth; - if (dbContext.CreateBucketVersion(BucketVersion) == 0) - throw new Exception($"Unable to create new Bucket Version.{Environment.NewLine}" + - $"{Environment.NewLine}" + - $"Bucket ID: {BucketVersion.BucketId}" + - $"Bucket Version: {BucketVersion.Version}" + - $"Bucket Version Start Date: {BucketVersion.ValidFrom.ToShortDateString()}"); - } + BucketVersion.Version++; + BucketVersion.BucketVersionId = 0; + BucketVersion.ValidFrom = _currentYearMonth; + if (dbContext.CreateBucketVersion(BucketVersion) == 0) + throw new Exception($"Unable to create new Bucket Version.{Environment.NewLine}" + + $"{Environment.NewLine}" + + $"Bucket ID: {BucketVersion.BucketId}" + + $"Bucket Version: {BucketVersion.Version}" + + $"Bucket Version Start Date: {BucketVersion.ValidFrom.ToShortDateString()}"); } - transaction.Commit(); - return new ViewModelOperationResult(true, true); - } - catch (Exception e) - { - transaction.Rollback(); - return new ViewModelOperationResult( - false, - $"Error during database update: {e.Message}", - true); } + transaction.Commit(); + return new ViewModelOperationResult(true, true); + } + catch (Exception e) + { + transaction.Rollback(); + return new ViewModelOperationResult( + false, + $"Error during database update: {e.Message}", + true); } } } + } - /// - /// Helper method to create a new record in the database based on User input - /// - /// Creation starts once Enter key is pressed - /// Recalculates figures after database operations - /// Pressed key - /// Object which contains information and results of this method - public ViewModelOperationResult HandleInOutInput(string key) + /// + /// Helper method to create a new record in the database based on User input + /// + /// Creation starts once Enter key is pressed + /// Recalculates figures after database operations + /// Pressed key + /// Object which contains information and results of this method + public ViewModelOperationResult HandleInOutInput(string key) + { + if (key != "Enter") return new ViewModelOperationResult(true); + try { - if (key != "Enter") return new ViewModelOperationResult(true); - try - { - using (var dbContext = new DatabaseContext(_dbOptions)) - { - var newMovement = new BucketMovement(Bucket, InOut, _currentYearMonth); - if (dbContext.CreateBucketMovement(newMovement) == 0) - throw new Exception($"Unable to create new Bucket Movement.{Environment.NewLine}" + - $"{Environment.NewLine}" + - $"Bucket ID: {newMovement.BucketId}" + - $"Amount: {newMovement.Amount}" + - $"Movement Date: {newMovement.MovementDate.ToShortDateString()}"); - } - //ViewModelReloadRequired?.Invoke(this); - CalculateValues(); - return new ViewModelOperationResult(true); - } - catch (Exception e) + using (var dbContext = new DatabaseContext(_dbOptions)) { - return new ViewModelOperationResult(false, $"Error during database update: {e.Message}"); + var newMovement = new BucketMovement(Bucket, InOut, _currentYearMonth); + if (dbContext.CreateBucketMovement(newMovement) == 0) + throw new Exception($"Unable to create new Bucket Movement.{Environment.NewLine}" + + $"{Environment.NewLine}" + + $"Bucket ID: {newMovement.BucketId}" + + $"Amount: {newMovement.Amount}" + + $"Movement Date: {newMovement.MovementDate.ToShortDateString()}"); } + //ViewModelReloadRequired?.Invoke(this); + CalculateValues(); + return new ViewModelOperationResult(true); + } + catch (Exception e) + { + return new ViewModelOperationResult(false, $"Error during database update: {e.Message}"); } } } diff --git a/OpenBudgeteer.Core/ViewModels/ItemViewModels/MappingRuleViewModelItem.cs b/OpenBudgeteer.Core/ViewModels/ItemViewModels/MappingRuleViewModelItem.cs index acac5a6..492a80f 100644 --- a/OpenBudgeteer.Core/ViewModels/ItemViewModels/MappingRuleViewModelItem.cs +++ b/OpenBudgeteer.Core/ViewModels/ItemViewModels/MappingRuleViewModelItem.cs @@ -1,65 +1,61 @@ -using System; -using System.Collections.Generic; -using System.Text; -using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore; using OpenBudgeteer.Core.Common.Database; using OpenBudgeteer.Core.Models; -namespace OpenBudgeteer.Core.ViewModels.ItemViewModels +namespace OpenBudgeteer.Core.ViewModels.ItemViewModels; + +public class MappingRuleViewModelItem : ViewModelBase { - public class MappingRuleViewModelItem : ViewModelBase + private MappingRule _mappingRule; + /// + /// Reference to model object in the database + /// + public MappingRule MappingRule { - private MappingRule _mappingRule; - /// - /// Reference to model object in the database - /// - public MappingRule MappingRule - { - get => _mappingRule; - set => Set(ref _mappingRule, value); - } + get => _mappingRule; + set => Set(ref _mappingRule, value); + } - private string _ruleOutput; - /// - /// Helper property to generate a readable output for - /// - public string RuleOutput - { - get => _ruleOutput; - set => Set(ref _ruleOutput, value); - } - - private readonly DbContextOptions _dbOptions; + private string _ruleOutput; + /// + /// Helper property to generate a readable output for + /// + public string RuleOutput + { + get => _ruleOutput; + set => Set(ref _ruleOutput, value); + } + + private readonly DbContextOptions _dbOptions; - /// - /// Basic constructor - /// - /// Options to connect to a database - public MappingRuleViewModelItem(DbContextOptions dbOptions) - { - _dbOptions = dbOptions; - } + /// + /// Basic constructor + /// + /// Options to connect to a database + public MappingRuleViewModelItem(DbContextOptions dbOptions) + { + _dbOptions = dbOptions; + } - /// - /// Initialize ViewModel with an existing object - /// - /// Options to connect to a database - /// MappingRule instance - public MappingRuleViewModelItem(DbContextOptions dbOptions, MappingRule mappingRule) : this(dbOptions) - { - MappingRule = mappingRule; - GenerateRuleOutput(); - } + /// + /// Initialize ViewModel with an existing object + /// + /// Options to connect to a database + /// MappingRule instance + public MappingRuleViewModelItem(DbContextOptions dbOptions, MappingRule mappingRule) : this(dbOptions) + { + MappingRule = mappingRule; + GenerateRuleOutput(); + } - /// - /// Translates object into a readable format - /// - public void GenerateRuleOutput() - { - RuleOutput = MappingRule == null ? string.Empty : - $"{MappingRule.ComparisonFieldOutput} " + - $"{MappingRule.ComparisionTypeOutput} " + - $"{MappingRule.ComparisionValue}"; - } + /// + /// Translates object into a readable format + /// + public void GenerateRuleOutput() + { + RuleOutput = MappingRule == null ? string.Empty : + $"{MappingRule.ComparisonFieldOutput} " + + $"{MappingRule.ComparisionTypeOutput} " + + $"{MappingRule.ComparisionValue}"; } } diff --git a/OpenBudgeteer.Core/ViewModels/ItemViewModels/MonthlyBucketExpensesReportViewModelItem.cs b/OpenBudgeteer.Core/ViewModels/ItemViewModels/MonthlyBucketExpensesReportViewModelItem.cs index b167ab0..932b7a4 100644 --- a/OpenBudgeteer.Core/ViewModels/ItemViewModels/MonthlyBucketExpensesReportViewModelItem.cs +++ b/OpenBudgeteer.Core/ViewModels/ItemViewModels/MonthlyBucketExpensesReportViewModelItem.cs @@ -1,41 +1,38 @@ using System; -using System.Collections.Generic; using System.Collections.ObjectModel; -using System.Text; -namespace OpenBudgeteer.Core.ViewModels.ItemViewModels +namespace OpenBudgeteer.Core.ViewModels.ItemViewModels; + +/// +/// Helper class for Reports showing monthly Bucket expenses +/// +public class MonthlyBucketExpensesReportViewModelItem : ViewModelBase { + private string _bucketName; /// - /// Helper class for Reports showing monthly Bucket expenses + /// Name of the Bucket /// - public class MonthlyBucketExpensesReportViewModelItem : ViewModelBase + public string BucketName { - private string _bucketName; - /// - /// Name of the Bucket - /// - public string BucketName - { - get => _bucketName; - set => Set(ref _bucketName, value); - } + get => _bucketName; + set => Set(ref _bucketName, value); + } - private ObservableCollection> _monthlyResults; - /// - /// Collection of the results for the report - /// - public ObservableCollection> MonthlyResults - { - get => _monthlyResults; - set => Set(ref _monthlyResults, value); - } + private ObservableCollection> _monthlyResults; + /// + /// Collection of the results for the report + /// + public ObservableCollection> MonthlyResults + { + get => _monthlyResults; + set => Set(ref _monthlyResults, value); + } - /// - /// Basic constructor - /// - public MonthlyBucketExpensesReportViewModelItem() - { - MonthlyResults = new ObservableCollection>(); - } + /// + /// Basic constructor + /// + public MonthlyBucketExpensesReportViewModelItem() + { + MonthlyResults = new ObservableCollection>(); } } diff --git a/OpenBudgeteer.Core/ViewModels/ItemViewModels/PartialBucketViewModelItem.cs b/OpenBudgeteer.Core/ViewModels/ItemViewModels/PartialBucketViewModelItem.cs index d518cec..81a68cd 100644 --- a/OpenBudgeteer.Core/ViewModels/ItemViewModels/PartialBucketViewModelItem.cs +++ b/OpenBudgeteer.Core/ViewModels/ItemViewModels/PartialBucketViewModelItem.cs @@ -3,134 +3,131 @@ using OpenBudgeteer.Core.Common.EventClasses; using OpenBudgeteer.Core.Models; using System; -using System.Collections.Generic; using System.Collections.ObjectModel; using System.Linq; -using System.Text; -namespace OpenBudgeteer.Core.ViewModels.ItemViewModels +namespace OpenBudgeteer.Core.ViewModels.ItemViewModels; + +/// +/// Helper ViewModel to handle the multi-assignment of Buckets to one +/// +public class PartialBucketViewModelItem : ViewModelBase { + private Bucket _selectedBucket; /// - /// Helper ViewModel to handle the multi-assignment of Buckets to one + /// Affected Bucket /// - public class PartialBucketViewModelItem : ViewModelBase + public Bucket SelectedBucket { - private Bucket _selectedBucket; - /// - /// Affected Bucket - /// - public Bucket SelectedBucket - { - get => _selectedBucket; - set => Set(ref _selectedBucket, value); - } + get => _selectedBucket; + set => Set(ref _selectedBucket, value); + } - private string _selectedBucketOutput; - /// - /// Helper property to generate an output for the Bucket including the assigned amount - /// - public string SelectedBucketOutput - { - get => _selectedBucketOutput; - set => Set(ref _selectedBucketOutput, value); - } + private string _selectedBucketOutput; + /// + /// Helper property to generate an output for the Bucket including the assigned amount + /// + public string SelectedBucketOutput + { + get => _selectedBucketOutput; + set => Set(ref _selectedBucketOutput, value); + } - private decimal _amount; - /// - /// Money that will be assigned to this Bucket - /// - public decimal Amount + private decimal _amount; + /// + /// Money that will be assigned to this Bucket + /// + public decimal Amount + { + get => _amount; + set { - get => _amount; - set - { - Set(ref _amount, value); - AmountChanged?.Invoke(this, new AmountChangedArgs(this, value)); - } + Set(ref _amount, value); + AmountChanged?.Invoke(this, new AmountChangedArgs(this, value)); } + } - private ObservableCollection _availableBuckets; - /// - /// Helper collection with all available Buckets - /// - public ObservableCollection AvailableBuckets - { - get => _availableBuckets; - set => Set(ref _availableBuckets, value); - } + private ObservableCollection _availableBuckets; + /// + /// Helper collection with all available Buckets + /// + public ObservableCollection AvailableBuckets + { + get => _availableBuckets; + set => Set(ref _availableBuckets, value); + } - /// - /// EventHandler which should be invoked once amount assigned to this Bucket has been changed. Can be used - /// to start further consistency checks and other calculations based on this change - /// - public event EventHandler AmountChanged; - /// - /// EventHandler which should be invoked in case this instance should start its deletion process. Can be used - /// in case the way how this instance will be deleted is handled outside of this class - /// - public event EventHandler DeleteAssignmentRequest; - - /// - /// Basic constructor - /// - /// Options to connect to a database - /// Current YearMonth - public PartialBucketViewModelItem(DbContextOptions dbOptions, DateTime yearMonth) + /// + /// EventHandler which should be invoked once amount assigned to this Bucket has been changed. Can be used + /// to start further consistency checks and other calculations based on this change + /// + public event EventHandler AmountChanged; + /// + /// EventHandler which should be invoked in case this instance should start its deletion process. Can be used + /// in case the way how this instance will be deleted is handled outside of this class + /// + public event EventHandler DeleteAssignmentRequest; + + /// + /// Basic constructor + /// + /// Options to connect to a database + /// Current YearMonth + public PartialBucketViewModelItem(DbContextOptions dbOptions, DateTime yearMonth) + { + AvailableBuckets = new ObservableCollection { - AvailableBuckets = new ObservableCollection - { - new Bucket {BucketId = 0, BucketGroupId = 0, Name = "No Selection"} - }; - // Add empty Bucket for empty pre-selection - using (var dbContext = new DatabaseContext(dbOptions)) + new Bucket {BucketId = 0, BucketGroupId = 0, Name = "No Selection"} + }; + // Add empty Bucket for empty pre-selection + using (var dbContext = new DatabaseContext(dbOptions)) + { + foreach (var availableBucket in dbContext.Bucket.Where(i => i.BucketId <= 2)) { - foreach (var availableBucket in dbContext.Bucket.Where(i => i.BucketId <= 2)) - { - AvailableBuckets.Add(availableBucket); - } + AvailableBuckets.Add(availableBucket); + } - var query = dbContext.Bucket - .Where(i => i.BucketId > 2 && - i.ValidFrom <= yearMonth && - (i.IsInactive == false || - (i.IsInactive && i.IsInactiveFrom > yearMonth))) - .OrderBy(i => i.Name); + var query = dbContext.Bucket + .Where(i => i.BucketId > 2 && + i.ValidFrom <= yearMonth && + (i.IsInactive == false || + (i.IsInactive && i.IsInactiveFrom > yearMonth))) + .OrderBy(i => i.Name); - foreach (var availableBucket in query.ToList()) - { - AvailableBuckets.Add(availableBucket); - } - } - SelectedBucket = AvailableBuckets.First(); - } + foreach (var availableBucket in query.ToList()) + { + AvailableBuckets.Add(availableBucket); + } + } + SelectedBucket = AvailableBuckets.First(); + } - /// - /// Initialize ViewModel based on an existing object and the final amount to be assigned - /// - /// Options to connect to a database - /// Current YearMonth - /// Bucket instance - /// Amount to be assigned to this Bucket - public PartialBucketViewModelItem(DbContextOptions dbOptions, DateTime yearMonth, Bucket bucket, decimal amount) : this(dbOptions, yearMonth) + /// + /// Initialize ViewModel based on an existing object and the final amount to be assigned + /// + /// Options to connect to a database + /// Current YearMonth + /// Bucket instance + /// Amount to be assigned to this Bucket + public PartialBucketViewModelItem(DbContextOptions dbOptions, DateTime yearMonth, Bucket bucket, decimal amount) : this(dbOptions, yearMonth) + { + Amount = amount; + foreach (var availableBucket in AvailableBuckets) { - Amount = amount; - foreach (var availableBucket in AvailableBuckets) + if (availableBucket.BucketId == bucket.BucketId) { - if (availableBucket.BucketId == bucket.BucketId) - { - SelectedBucket = availableBucket; - } + SelectedBucket = availableBucket; } - // Pre-select "No Selection" Bucket if no Bucket was found - if (SelectedBucket == null) SelectedBucket = AvailableBuckets.First(); } + // Pre-select "No Selection" Bucket if no Bucket was found + if (SelectedBucket == null) SelectedBucket = AvailableBuckets.First(); + } - /// - /// Triggers - /// - public void DeleteBucket() - { - DeleteAssignmentRequest?.Invoke(this, new DeleteAssignmentRequestArgs(this)); - } + /// + /// Triggers + /// + public void DeleteBucket() + { + DeleteAssignmentRequest?.Invoke(this, new DeleteAssignmentRequestArgs(this)); } } diff --git a/OpenBudgeteer.Core/ViewModels/ItemViewModels/RuleSetViewModelItem.cs b/OpenBudgeteer.Core/ViewModels/ItemViewModels/RuleSetViewModelItem.cs index a5773ce..52ec7c3 100644 --- a/OpenBudgeteer.Core/ViewModels/ItemViewModels/RuleSetViewModelItem.cs +++ b/OpenBudgeteer.Core/ViewModels/ItemViewModels/RuleSetViewModelItem.cs @@ -1,235 +1,232 @@ using System; -using System.Collections.Generic; using System.Collections.ObjectModel; using System.Linq; -using System.Text; using Microsoft.EntityFrameworkCore; using OpenBudgeteer.Core.Common; using OpenBudgeteer.Core.Common.Database; using OpenBudgeteer.Core.Models; -namespace OpenBudgeteer.Core.ViewModels.ItemViewModels +namespace OpenBudgeteer.Core.ViewModels.ItemViewModels; + +public class RuleSetViewModelItem : ViewModelBase { - public class RuleSetViewModelItem : ViewModelBase + private BucketRuleSet _ruleSet; + /// + /// Reference to model object in the database + /// + public BucketRuleSet RuleSet { - private BucketRuleSet _ruleSet; - /// - /// Reference to model object in the database - /// - public BucketRuleSet RuleSet - { - get => _ruleSet; - set => Set(ref _ruleSet, value); - } + get => _ruleSet; + set => Set(ref _ruleSet, value); + } - private Bucket _targetBucket; - /// - /// Bucket to which this RuleSet applies - /// - public Bucket TargetBucket - { - get => _targetBucket; - set => Set(ref _targetBucket, value); - } + private Bucket _targetBucket; + /// + /// Bucket to which this RuleSet applies + /// + public Bucket TargetBucket + { + get => _targetBucket; + set => Set(ref _targetBucket, value); + } - private bool _inModification; - /// - /// Helper property to check if the RuleSet is currently modified - /// - public bool InModification - { - get => _inModification; - set => Set(ref _inModification, value); - } + private bool _inModification; + /// + /// Helper property to check if the RuleSet is currently modified + /// + public bool InModification + { + get => _inModification; + set => Set(ref _inModification, value); + } - private bool _isHovered; - /// - /// Helper property to check if the cursor hovers over the entry in the UI - /// - public bool IsHovered - { - get => _isHovered; - set => Set(ref _isHovered, value); - } + private bool _isHovered; + /// + /// Helper property to check if the cursor hovers over the entry in the UI + /// + public bool IsHovered + { + get => _isHovered; + set => Set(ref _isHovered, value); + } - private ObservableCollection _mappingRules; - /// - /// Collection of MappingRules assigned to this RuleSet - /// - public ObservableCollection MappingRules - { - get => _mappingRules; - set => Set(ref _mappingRules, value); - } + private ObservableCollection _mappingRules; + /// + /// Collection of MappingRules assigned to this RuleSet + /// + public ObservableCollection MappingRules + { + get => _mappingRules; + set => Set(ref _mappingRules, value); + } - private ObservableCollection _availableBuckets; - /// - /// Helper collection to list all existing Buckets - /// - public ObservableCollection AvailableBuckets - { - get => _availableBuckets; - set => Set(ref _availableBuckets, value); - } + private ObservableCollection _availableBuckets; + /// + /// Helper collection to list all existing Buckets + /// + public ObservableCollection AvailableBuckets + { + get => _availableBuckets; + set => Set(ref _availableBuckets, value); + } - private readonly DbContextOptions _dbOptions; - private RuleSetViewModelItem _oldRuleSetViewModelItem; + private readonly DbContextOptions _dbOptions; + private RuleSetViewModelItem _oldRuleSetViewModelItem; - /// - /// Basic constructor - /// - /// Options to connect to a database - public RuleSetViewModelItem(DbContextOptions dbOptions) + /// + /// Basic constructor + /// + /// Options to connect to a database + public RuleSetViewModelItem(DbContextOptions dbOptions) + { + MappingRules = new ObservableCollection(); + AvailableBuckets = new ObservableCollection(); + RuleSet = new BucketRuleSet(); + TargetBucket = new Bucket(); + _dbOptions = dbOptions; + AvailableBuckets.Add(new Bucket { - MappingRules = new ObservableCollection(); - AvailableBuckets = new ObservableCollection(); - RuleSet = new BucketRuleSet(); - TargetBucket = new Bucket(); - _dbOptions = dbOptions; - AvailableBuckets.Add(new Bucket - { - BucketId = 0, - BucketGroupId = 0, - Name = "No Selection" - }); - using (var dbContext = new DatabaseContext(_dbOptions)) + BucketId = 0, + BucketGroupId = 0, + Name = "No Selection" + }); + using (var dbContext = new DatabaseContext(_dbOptions)) + { + foreach (var availableBucket in dbContext.Bucket.Where(i => i.BucketId <= 2)) { - foreach (var availableBucket in dbContext.Bucket.Where(i => i.BucketId <= 2)) - { - AvailableBuckets.Add(availableBucket); - } + AvailableBuckets.Add(availableBucket); + } - var query = dbContext.Bucket - .Where(i => i.BucketId > 2 && !i.IsInactive) - .OrderBy(i => i.Name); + var query = dbContext.Bucket + .Where(i => i.BucketId > 2 && !i.IsInactive) + .OrderBy(i => i.Name); - foreach (var availableBucket in query.ToList()) - { - AvailableBuckets.Add(availableBucket); - } + foreach (var availableBucket in query.ToList()) + { + AvailableBuckets.Add(availableBucket); } } + } - /// - /// Initialize ViewModel based on an existing - /// - /// Options to connect to a database - /// RuleSet instance - public RuleSetViewModelItem(DbContextOptions dbOptions, BucketRuleSet bucketRuleSet) : - this(dbOptions) + /// + /// Initialize ViewModel based on an existing + /// + /// Options to connect to a database + /// RuleSet instance + public RuleSetViewModelItem(DbContextOptions dbOptions, BucketRuleSet bucketRuleSet) : + this(dbOptions) + { + // Make a copy of the object to prevent any double Bindings + RuleSet = new BucketRuleSet() { - // Make a copy of the object to prevent any double Bindings - RuleSet = new BucketRuleSet() - { - BucketRuleSetId = bucketRuleSet.BucketRuleSetId, - Name = bucketRuleSet.Name, - Priority = bucketRuleSet.Priority, - TargetBucketId = bucketRuleSet.TargetBucketId - }; - using (var dbContext = new DatabaseContext(_dbOptions)) + BucketRuleSetId = bucketRuleSet.BucketRuleSetId, + Name = bucketRuleSet.Name, + Priority = bucketRuleSet.Priority, + TargetBucketId = bucketRuleSet.TargetBucketId + }; + using (var dbContext = new DatabaseContext(_dbOptions)) + { + TargetBucket = dbContext.Bucket.FirstOrDefault(i => i.BucketId == bucketRuleSet.TargetBucketId); + foreach (var mappingRule in dbContext.MappingRule.Where(i => i.BucketRuleSetId == bucketRuleSet.BucketRuleSetId)) { - TargetBucket = dbContext.Bucket.FirstOrDefault(i => i.BucketId == bucketRuleSet.TargetBucketId); - foreach (var mappingRule in dbContext.MappingRule.Where(i => i.BucketRuleSetId == bucketRuleSet.BucketRuleSetId)) - { - MappingRules.Add(new MappingRuleViewModelItem(_dbOptions, mappingRule)); - } + MappingRules.Add(new MappingRuleViewModelItem(_dbOptions, mappingRule)); } } + } - /// - /// Helper method to start modification process - /// - public void StartModification() - { - _oldRuleSetViewModelItem = new RuleSetViewModelItem(_dbOptions, RuleSet); - InModification = true; - } + /// + /// Helper method to start modification process + /// + public void StartModification() + { + _oldRuleSetViewModelItem = new RuleSetViewModelItem(_dbOptions, RuleSet); + InModification = true; + } - /// - /// Stops modification process and restores old values - /// - public void CancelModification() - { - RuleSet = _oldRuleSetViewModelItem.RuleSet; - MappingRules = _oldRuleSetViewModelItem.MappingRules; - InModification = false; - _oldRuleSetViewModelItem = null; - } + /// + /// Stops modification process and restores old values + /// + public void CancelModification() + { + RuleSet = _oldRuleSetViewModelItem.RuleSet; + MappingRules = _oldRuleSetViewModelItem.MappingRules; + InModification = false; + _oldRuleSetViewModelItem = null; + } - /// - /// Creates an initial and adds it to the - /// - public void AddEmptyMappingRule() - { - MappingRules.Add(new MappingRuleViewModelItem(_dbOptions, new MappingRule())); - } + /// + /// Creates an initial and adds it to the + /// + public void AddEmptyMappingRule() + { + MappingRules.Add(new MappingRuleViewModelItem(_dbOptions, new MappingRule())); + } - /// - /// Creates or updates records in the database based on and objects - /// - /// Object which contains information and results of this method - public ViewModelOperationResult CreateUpdateRuleSetItem() + /// + /// Creates or updates records in the database based on and objects + /// + /// Object which contains information and results of this method + public ViewModelOperationResult CreateUpdateRuleSetItem() + { + using (var dbContext = new DatabaseContext(_dbOptions)) { - using (var dbContext = new DatabaseContext(_dbOptions)) + using (var dbTransaction = dbContext.Database.BeginTransaction()) { - using (var dbTransaction = dbContext.Database.BeginTransaction()) + try { - try + if (RuleSet.BucketRuleSetId == 0) { - if (RuleSet.BucketRuleSetId == 0) - { - // CREATE - if (dbContext.CreateBucketRuleSet(RuleSet) == 0) - throw new Exception("Rule could not be created in database."); - foreach (var mappingRule in MappingRules) - { - mappingRule.MappingRule.BucketRuleSetId = RuleSet.BucketRuleSetId; - } - } - else + // CREATE + if (dbContext.CreateBucketRuleSet(RuleSet) == 0) + throw new Exception("Rule could not be created in database."); + foreach (var mappingRule in MappingRules) { - // UPDATE - dbContext.DeleteMappingRules(dbContext.MappingRule.Where(i => - i.BucketRuleSetId == RuleSet.BucketRuleSetId)); - - dbContext.UpdateBucketRuleSet(RuleSet); - foreach (var mappingRule in MappingRules) - { - mappingRule.GenerateRuleOutput(); - } + mappingRule.MappingRule.BucketRuleSetId = RuleSet.BucketRuleSetId; } + } + else + { + // UPDATE + dbContext.DeleteMappingRules(dbContext.MappingRule.Where(i => + i.BucketRuleSetId == RuleSet.BucketRuleSetId)); - foreach (var mappingRuleViewModelItem in MappingRules) + dbContext.UpdateBucketRuleSet(RuleSet); + foreach (var mappingRule in MappingRules) { - mappingRuleViewModelItem.MappingRule.MappingRuleId = 0; + mappingRule.GenerateRuleOutput(); } - dbContext.CreateMappingRules(MappingRules.Select(i => i.MappingRule).ToList()); - - dbTransaction.Commit(); - _oldRuleSetViewModelItem = null; - InModification = false; - - return new ViewModelOperationResult(true); } - catch (Exception e) + + foreach (var mappingRuleViewModelItem in MappingRules) { - dbTransaction.Rollback(); - return new ViewModelOperationResult(false, $"Errors during database update: {e.Message}"); + mappingRuleViewModelItem.MappingRule.MappingRuleId = 0; } + dbContext.CreateMappingRules(MappingRules.Select(i => i.MappingRule).ToList()); + + dbTransaction.Commit(); + _oldRuleSetViewModelItem = null; + InModification = false; + + return new ViewModelOperationResult(true); + } + catch (Exception e) + { + dbTransaction.Rollback(); + return new ViewModelOperationResult(false, $"Errors during database update: {e.Message}"); } - } + } + } - /// - /// Deletes passed MappingRule from the collection - /// - /// MappingRule that needs to be removed - public void DeleteMappingRule(MappingRuleViewModelItem mappingRule) - { - //Note: Doesn't require any database updates as this will be done during CreateUpdateRuleSetItem - MappingRules.Remove(mappingRule); - if (MappingRules.Count == 0) AddEmptyMappingRule(); - } + /// + /// Deletes passed MappingRule from the collection + /// + /// MappingRule that needs to be removed + public void DeleteMappingRule(MappingRuleViewModelItem mappingRule) + { + //Note: Doesn't require any database updates as this will be done during CreateUpdateRuleSetItem + MappingRules.Remove(mappingRule); + if (MappingRules.Count == 0) AddEmptyMappingRule(); } } diff --git a/OpenBudgeteer.Core/ViewModels/ItemViewModels/TransactionViewModelItem.cs b/OpenBudgeteer.Core/ViewModels/ItemViewModels/TransactionViewModelItem.cs index 89f0784..041b118 100644 --- a/OpenBudgeteer.Core/ViewModels/ItemViewModels/TransactionViewModelItem.cs +++ b/OpenBudgeteer.Core/ViewModels/ItemViewModels/TransactionViewModelItem.cs @@ -3,602 +3,599 @@ using OpenBudgeteer.Core.Common.EventClasses; using OpenBudgeteer.Core.Models; using System; -using System.Collections.Generic; using System.Collections.ObjectModel; using System.Linq; -using System.Text; using System.Threading.Tasks; using OpenBudgeteer.Core.Common; -namespace OpenBudgeteer.Core.ViewModels.ItemViewModels +namespace OpenBudgeteer.Core.ViewModels.ItemViewModels; + +public class TransactionViewModelItem : ViewModelBase { - public class TransactionViewModelItem : ViewModelBase + private BankTransaction _transaction; + /// + /// Reference to model object in the database + /// + public BankTransaction Transaction { - private BankTransaction _transaction; - /// - /// Reference to model object in the database - /// - public BankTransaction Transaction - { - get => _transaction; - internal set => Set(ref _transaction, value); - } + get => _transaction; + internal set => Set(ref _transaction, value); + } - private Account _selectedAccount; - /// - /// Account where the Transaction is assigned to - /// - public Account SelectedAccount - { - get => _selectedAccount; - set => Set(ref _selectedAccount, value); - } + private Account _selectedAccount; + /// + /// Account where the Transaction is assigned to + /// + public Account SelectedAccount + { + get => _selectedAccount; + set => Set(ref _selectedAccount, value); + } - private bool _inModification; - /// - /// Helper property to check if the Transaction is currently modified - /// - public bool InModification - { - get => _inModification; - set => Set(ref _inModification, value); - } + private bool _inModification; + /// + /// Helper property to check if the Transaction is currently modified + /// + public bool InModification + { + get => _inModification; + set => Set(ref _inModification, value); + } - private bool _isHovered; - /// - /// Helper property to check if the cursor hovers over the entry in the UI - /// - public bool IsHovered - { - get => _isHovered; - set => Set(ref _isHovered, value); - } + private bool _isHovered; + /// + /// Helper property to check if the cursor hovers over the entry in the UI + /// + public bool IsHovered + { + get => _isHovered; + set => Set(ref _isHovered, value); + } - private ObservableCollection _buckets; - /// - /// Collection of Buckets which are assigned to this Transaction - /// - public ObservableCollection Buckets - { - get => _buckets; - set => Set(ref _buckets, value); - } + private ObservableCollection _buckets; + /// + /// Collection of Buckets which are assigned to this Transaction + /// + public ObservableCollection Buckets + { + get => _buckets; + set => Set(ref _buckets, value); + } - private ObservableCollection _availableAccounts; - /// - /// Helper collection to list all existing Account - /// - public ObservableCollection AvailableAccounts - { - get => _availableAccounts; - set => Set(ref _availableAccounts, value); - } + private ObservableCollection _availableAccounts; + /// + /// Helper collection to list all existing Account + /// + public ObservableCollection AvailableAccounts + { + get => _availableAccounts; + set => Set(ref _availableAccounts, value); + } - private readonly DbContextOptions _dbOptions; - private readonly YearMonthSelectorViewModel _yearMonthViewModel; - private TransactionViewModelItem _oldTransactionViewModelItem; + private readonly DbContextOptions _dbOptions; + private readonly YearMonthSelectorViewModel _yearMonthViewModel; + private TransactionViewModelItem _oldTransactionViewModelItem; - /// - /// Basic constructor - /// - public TransactionViewModelItem() - { - Transaction = new BankTransaction(); - Buckets = new ObservableCollection(); - AvailableAccounts = new ObservableCollection(); - } + /// + /// Basic constructor + /// + public TransactionViewModelItem() + { + Transaction = new BankTransaction(); + Buckets = new ObservableCollection(); + AvailableAccounts = new ObservableCollection(); + } - /// - /// Basic constructor - /// - /// Options to connect to a database - /// YearMonth ViewModel instance - public TransactionViewModelItem(DbContextOptions dbOptions, YearMonthSelectorViewModel yearMonthViewModel) : this() + /// + /// Basic constructor + /// + /// Options to connect to a database + /// YearMonth ViewModel instance + public TransactionViewModelItem(DbContextOptions dbOptions, YearMonthSelectorViewModel yearMonthViewModel) : this() + { + _dbOptions = dbOptions; + _yearMonthViewModel = yearMonthViewModel; + + // Set initial TransactionDate in case of "Create new Transaction" + Transaction.TransactionDate = _yearMonthViewModel.CurrentMonth; + // Get all available Accounts for ComboBox selections + // Add empty Account for empty pre-selection + AvailableAccounts.Add(new Account { - _dbOptions = dbOptions; - _yearMonthViewModel = yearMonthViewModel; - - // Set initial TransactionDate in case of "Create new Transaction" - Transaction.TransactionDate = _yearMonthViewModel.CurrentMonth; - // Get all available Accounts for ComboBox selections - // Add empty Account for empty pre-selection - AvailableAccounts.Add(new Account - { - AccountId = 0, - IsActive = 1, - Name = "No Account" - }); - using (var dbContext = new DatabaseContext(_dbOptions)) + AccountId = 0, + IsActive = 1, + Name = "No Account" + }); + using (var dbContext = new DatabaseContext(_dbOptions)) + { + foreach (var account in dbContext.Account.Where(i => i.IsActive == 1)) { - foreach (var account in dbContext.Account.Where(i => i.IsActive == 1)) - { - AvailableAccounts.Add(account); - } - } - SelectedAccount = AvailableAccounts.First(); - } + AvailableAccounts.Add(account); + } + } + SelectedAccount = AvailableAccounts.First(); + } - /// - /// Initialize ViewModel with an existing object - /// - /// Options to connect to a database - /// YearMonth ViewModel instance - /// Transaction instance - /// Include assigned Buckets - public TransactionViewModelItem(DbContextOptions dbOptions, YearMonthSelectorViewModel yearMonthViewModel, BankTransaction transaction, bool withBuckets = true) : this(dbOptions, yearMonthViewModel) + /// + /// Initialize ViewModel with an existing object + /// + /// Options to connect to a database + /// YearMonth ViewModel instance + /// Transaction instance + /// Include assigned Buckets + public TransactionViewModelItem(DbContextOptions dbOptions, YearMonthSelectorViewModel yearMonthViewModel, BankTransaction transaction, bool withBuckets = true) : this(dbOptions, yearMonthViewModel) + { + if (withBuckets) { - if (withBuckets) + // Get all assigned Buckets for this transaction + using (var dbContext = new DatabaseContext(_dbOptions)) { - // Get all assigned Buckets for this transaction - using (var dbContext = new DatabaseContext(_dbOptions)) - { - var assignedBuckets = dbContext.BudgetedTransaction - .Where(i => i.TransactionId == transaction.TransactionId); + var assignedBuckets = dbContext.BudgetedTransaction + .Where(i => i.TransactionId == transaction.TransactionId); - if (assignedBuckets.Any()) + if (assignedBuckets.Any()) + { + // Create a PartialBucketViewModelItem for each assignment + foreach (var assignedBucket in assignedBuckets) { - // Create a PartialBucketViewModelItem for each assignment - foreach (var assignedBucket in assignedBuckets) + using (var bucketDbContext = new DatabaseContext(_dbOptions)) { - using (var bucketDbContext = new DatabaseContext(_dbOptions)) - { - var newItem = new PartialBucketViewModelItem(_dbOptions, - _yearMonthViewModel.CurrentMonth, - bucketDbContext.Bucket.FirstOrDefault(i => i.BucketId == assignedBucket.BucketId), - assignedBucket.Amount); - newItem.SelectedBucketOutput = - newItem.Amount != transaction.Amount ? $"{newItem.SelectedBucket.Name} ({newItem.Amount})" : newItem.SelectedBucket.Name; - Buckets.Add(newItem); - } + var newItem = new PartialBucketViewModelItem(_dbOptions, + _yearMonthViewModel.CurrentMonth, + bucketDbContext.Bucket.FirstOrDefault(i => i.BucketId == assignedBucket.BucketId), + assignedBucket.Amount); + newItem.SelectedBucketOutput = + newItem.Amount != transaction.Amount ? $"{newItem.SelectedBucket.Name} ({newItem.Amount})" : newItem.SelectedBucket.Name; + Buckets.Add(newItem); } } - else - { - // Most likely an imported Transaction where Bucket assignment still needs to be done - var newItem = new PartialBucketViewModelItem(_dbOptions, _yearMonthViewModel.CurrentMonth, new Bucket() { BucketId = 0 }, transaction.Amount); - Buckets.Add(newItem); - } } - - // Subscribe Event Handler for Amount Changes (must be always the last step) and assignment deletion requests - foreach (var bucket in Buckets) + else { - bucket.AmountChanged += CheckBucketAssignments; - bucket.DeleteAssignmentRequest += DeleteRequestedBucketAssignment; + // Most likely an imported Transaction where Bucket assignment still needs to be done + var newItem = new PartialBucketViewModelItem(_dbOptions, _yearMonthViewModel.CurrentMonth, new Bucket() { BucketId = 0 }, transaction.Amount); + Buckets.Add(newItem); } } - - // Make a copy of the object to prevent any double Bindings - Transaction = new BankTransaction - { - TransactionId = transaction.TransactionId, - AccountId = transaction.AccountId, - Amount = transaction.Amount, - Memo = transaction.Memo, - Payee = transaction.Payee, - TransactionDate = transaction.TransactionDate - }; - // Pre-selection the right account - using (var dbContext = new DatabaseContext(_dbOptions)) + + // Subscribe Event Handler for Amount Changes (must be always the last step) and assignment deletion requests + foreach (var bucket in Buckets) { - var account = dbContext.Account.First(i => i.AccountId == transaction.AccountId); - if (account != null && account.IsActive == 0) - { - account.Name += " (Inactive)"; - AvailableAccounts.Add(account); - } - SelectedAccount = account; + bucket.AmountChanged += CheckBucketAssignments; + bucket.DeleteAssignmentRequest += DeleteRequestedBucketAssignment; } - // Pre-select empty Account if no Account was found (for new BankTransaction()) - SelectedAccount ??= AvailableAccounts.First(); - } - - /// - /// Initialize ViewModel and transform passed into a - /// - /// BucketMovement which will be transformed - public TransactionViewModelItem(BucketMovement bucketMovement) : this() + + // Make a copy of the object to prevent any double Bindings + Transaction = new BankTransaction { - // Simulate a BankTransaction based on BucketMovement - Transaction = new BankTransaction - { - TransactionId = 0, - AccountId = 0, - Amount = bucketMovement.Amount, - Memo = "Bucket Movement", - Payee = string.Empty, - TransactionDate = bucketMovement.MovementDate, - }; - - // Simulate Account - SelectedAccount = new Account + TransactionId = transaction.TransactionId, + AccountId = transaction.AccountId, + Amount = transaction.Amount, + Memo = transaction.Memo, + Payee = transaction.Payee, + TransactionDate = transaction.TransactionDate + }; + // Pre-selection the right account + using (var dbContext = new DatabaseContext(_dbOptions)) + { + var account = dbContext.Account.First(i => i.AccountId == transaction.AccountId); + if (account != null && account.IsActive == 0) { - AccountId = 0, - IsActive = 1, - Name = string.Empty - }; + account.Name += " (Inactive)"; + AvailableAccounts.Add(account); + } + SelectedAccount = account; } + // Pre-select empty Account if no Account was found (for new BankTransaction()) + SelectedAccount ??= AvailableAccounts.First(); + + } - /// - /// Initialize and return a new ViewModel based on an existing object including - /// assigned Buckets - /// - /// Options to connect to a database - /// YearMonth ViewModel instance - /// Transaction instance - /// New ViewModel instance - public static async Task CreateAsync(DbContextOptions dbOptions, YearMonthSelectorViewModel yearMonthViewModel, BankTransaction transaction) + /// + /// Initialize ViewModel and transform passed into a + /// + /// BucketMovement which will be transformed + public TransactionViewModelItem(BucketMovement bucketMovement) : this() + { + // Simulate a BankTransaction based on BucketMovement + Transaction = new BankTransaction { - return await Task.Run(() => new TransactionViewModelItem(dbOptions, yearMonthViewModel, transaction)); - } - - /// - /// Initialize and return a new ViewModel based on an existing object without - /// assigned Buckets - /// - /// Options to connect to a database - /// YearMonth ViewModel instance - /// Transaction instance - /// New ViewModel instance - public static async Task CreateWithoutBucketsAsync(DbContextOptions dbOptions, YearMonthSelectorViewModel yearMonthViewModel, BankTransaction transaction) + TransactionId = 0, + AccountId = 0, + Amount = bucketMovement.Amount, + Memo = "Bucket Movement", + Payee = string.Empty, + TransactionDate = bucketMovement.MovementDate, + }; + + // Simulate Account + SelectedAccount = new Account { - return await Task.Run(() => new TransactionViewModelItem(dbOptions, yearMonthViewModel, transaction, false)); - } + AccountId = 0, + IsActive = 1, + Name = string.Empty + }; + } + + /// + /// Initialize and return a new ViewModel based on an existing object including + /// assigned Buckets + /// + /// Options to connect to a database + /// YearMonth ViewModel instance + /// Transaction instance + /// New ViewModel instance + public static async Task CreateAsync(DbContextOptions dbOptions, YearMonthSelectorViewModel yearMonthViewModel, BankTransaction transaction) + { + return await Task.Run(() => new TransactionViewModelItem(dbOptions, yearMonthViewModel, transaction)); + } - /// - /// Create and return a new ViewModel and transform passed into a - /// - /// BucketMovement which will be transformed - /// New ViewModel instance - public static async Task CreateFromBucketMovementAsync(BucketMovement bucketMovement) + /// + /// Initialize and return a new ViewModel based on an existing object without + /// assigned Buckets + /// + /// Options to connect to a database + /// YearMonth ViewModel instance + /// Transaction instance + /// New ViewModel instance + public static async Task CreateWithoutBucketsAsync(DbContextOptions dbOptions, YearMonthSelectorViewModel yearMonthViewModel, BankTransaction transaction) + { + return await Task.Run(() => new TransactionViewModelItem(dbOptions, yearMonthViewModel, transaction, false)); + } + + /// + /// Create and return a new ViewModel and transform passed into a + /// + /// BucketMovement which will be transformed + /// New ViewModel instance + public static async Task CreateFromBucketMovementAsync(BucketMovement bucketMovement) + { + return await Task.Run(() => new TransactionViewModelItem(bucketMovement)); + } + + /// + /// Event that checks amount for all assigned Buckets and creates or removes an "empty item" + /// + /// Object that has triggered the event + /// Event Arguments about changed amount + private void CheckBucketAssignments(object sender, AmountChangedArgs changedArgs) + { + // Check if this current event was triggered while updating the amount for the "emptyItem" + // Prevents Deadlock and StackOverflowException + if (changedArgs.Source.SelectedBucket == null || + changedArgs.Source.SelectedBucket.BucketId == 0) { - return await Task.Run(() => new TransactionViewModelItem(bucketMovement)); + return; } - /// - /// Event that checks amount for all assigned Buckets and creates or removes an "empty item" - /// - /// Object that has triggered the event - /// Event Arguments about changed amount - private void CheckBucketAssignments(object sender, AmountChangedArgs changedArgs) + // Calculate total amount assigned to any Bucket + decimal assignedAmount = 0; + foreach (var bucket in Buckets) { - // Check if this current event was triggered while updating the amount for the "emptyItem" - // Prevents Deadlock and StackOverflowException - if (changedArgs.Source.SelectedBucket == null || - changedArgs.Source.SelectedBucket.BucketId == 0) - { - return; - } + // ignore "emptyItem" where existing Bucket is not yet assigned + // this is the one where the amount has to be updated - // Calculate total amount assigned to any Bucket - decimal assignedAmount = 0; - foreach (var bucket in Buckets) + if (bucket.SelectedBucket != null && bucket.SelectedBucket.BucketId != 0) { - // ignore "emptyItem" where existing Bucket is not yet assigned - // this is the one where the amount has to be updated - - if (bucket.SelectedBucket != null && bucket.SelectedBucket.BucketId != 0) - { - assignedAmount += bucket.Amount; - } + assignedAmount += bucket.Amount; } + } - // Consistency check - if ((Transaction.Amount < 0 && assignedAmount > 0) || - (Transaction.Amount > 0 && assignedAmount < 0) || - // Check over-provisioning of amount assignment - (Transaction.Amount < 0 && Transaction.Amount - assignedAmount > 0) || - (Transaction.Amount > 0 && Transaction.Amount - assignedAmount < 0)) - { - return; // Inconsistency, better to do nothing, Error handling while saving - } + // Consistency check + if ((Transaction.Amount < 0 && assignedAmount > 0) || + (Transaction.Amount > 0 && assignedAmount < 0) || + // Check over-provisioning of amount assignment + (Transaction.Amount < 0 && Transaction.Amount - assignedAmount > 0) || + (Transaction.Amount > 0 && Transaction.Amount - assignedAmount < 0)) + { + return; // Inconsistency, better to do nothing, Error handling while saving + } - // Check if remaining amount left to be assigned to any Bucket - if (assignedAmount != Transaction.Amount) + // Check if remaining amount left to be assigned to any Bucket + if (assignedAmount != Transaction.Amount) + { + if (Buckets.Last().SelectedBucket != null && Buckets.Last().SelectedBucket.BucketId != 0) { - if (Buckets.Last().SelectedBucket != null && Buckets.Last().SelectedBucket.BucketId != 0) - { - // All items have a valid Bucket assignment, create a new "empty item" - AddEmptyBucketItem(Transaction.Amount - assignedAmount); - } - else - { - // "emptyItem" exists, update remaining amount to be assigned - Buckets.Last().Amount = Transaction.Amount - assignedAmount; - } + // All items have a valid Bucket assignment, create a new "empty item" + AddEmptyBucketItem(Transaction.Amount - assignedAmount); } - else if (Buckets.Last().SelectedBucket == null || - Buckets.Last().SelectedBucket.BucketId == 0) + else { - // Remove unnecessary "empty item" as amount is already assigned properly - Buckets.Remove(Buckets.Last()); + // "emptyItem" exists, update remaining amount to be assigned + Buckets.Last().Amount = Transaction.Amount - assignedAmount; } } - - /// - /// Event that handles the deletion of teh requested Bucket - /// - /// Object that has triggered the event - /// Event Arguments about deletion request - private void DeleteRequestedBucketAssignment(object sender, DeleteAssignmentRequestArgs args) + else if (Buckets.Last().SelectedBucket == null || + Buckets.Last().SelectedBucket.BucketId == 0) { - // Prevent deletion all last remaining BucketAssignment - if (Buckets.Count > 1) - { - Buckets.Remove(args.Source); - } + // Remove unnecessary "empty item" as amount is already assigned properly + Buckets.Remove(Buckets.Last()); } + } - /// - /// Creates an "empty item" with the passed amount - /// - /// Amount that will be assigned to the Bucket - private void AddEmptyBucketItem(decimal amount) + /// + /// Event that handles the deletion of teh requested Bucket + /// + /// Object that has triggered the event + /// Event Arguments about deletion request + private void DeleteRequestedBucketAssignment(object sender, DeleteAssignmentRequestArgs args) + { + // Prevent deletion all last remaining BucketAssignment + if (Buckets.Count > 1) { - // All items have a valid Bucket assignment, create a new "empty item" - var emptyItem = new PartialBucketViewModelItem(_dbOptions, _yearMonthViewModel.CurrentMonth, new Bucket(), amount); - emptyItem.AmountChanged += CheckBucketAssignments; - emptyItem.DeleteAssignmentRequest += DeleteRequestedBucketAssignment; - Buckets.Add(emptyItem); + Buckets.Remove(args.Source); } + } - /// - /// Creates or updates a record in the database based on object - /// - /// (Re)Creates also records for each assigned Bucket - /// Object which contains information and results of this method - private ViewModelOperationResult CreateOrUpdateTransaction() - { - var result = PerformConsistencyCheck(out var skipBucketAssignment); - if (!result.IsSuccessful) return result; + /// + /// Creates an "empty item" with the passed amount + /// + /// Amount that will be assigned to the Bucket + private void AddEmptyBucketItem(decimal amount) + { + // All items have a valid Bucket assignment, create a new "empty item" + var emptyItem = new PartialBucketViewModelItem(_dbOptions, _yearMonthViewModel.CurrentMonth, new Bucket(), amount); + emptyItem.AmountChanged += CheckBucketAssignments; + emptyItem.DeleteAssignmentRequest += DeleteRequestedBucketAssignment; + Buckets.Add(emptyItem); + } - using (var dbContext = new DatabaseContext(_dbOptions)) + /// + /// Creates or updates a record in the database based on object + /// + /// (Re)Creates also records for each assigned Bucket + /// Object which contains information and results of this method + private ViewModelOperationResult CreateOrUpdateTransaction() + { + var result = PerformConsistencyCheck(out var skipBucketAssignment); + if (!result.IsSuccessful) return result; + + using (var dbContext = new DatabaseContext(_dbOptions)) + { + using (var transaction = dbContext.Database.BeginTransaction()) { - using (var transaction = dbContext.Database.BeginTransaction()) + try { - try - { - var transactionId = Transaction.TransactionId; - Transaction.AccountId = SelectedAccount.AccountId; + var transactionId = Transaction.TransactionId; + Transaction.AccountId = SelectedAccount.AccountId; - if (transactionId != 0) - { - // Update BankTransaction in DB - dbContext.UpdateBankTransaction(Transaction); + if (transactionId != 0) + { + // Update BankTransaction in DB + dbContext.UpdateBankTransaction(Transaction); - // Delete all previous bucket assignments for transaction - var budgetedTransactions = dbContext.BudgetedTransaction - .Where(i => i.TransactionId == transactionId); - dbContext.DeleteBudgetedTransactions(budgetedTransactions); - } - else - { - // Create BankTransaction in DB - if (dbContext.CreateBankTransaction(Transaction) == 0) - throw new Exception("Transaction could not be created in database."); - } + // Delete all previous bucket assignments for transaction + var budgetedTransactions = dbContext.BudgetedTransaction + .Where(i => i.TransactionId == transactionId); + dbContext.DeleteBudgetedTransactions(budgetedTransactions); + } + else + { + // Create BankTransaction in DB + if (dbContext.CreateBankTransaction(Transaction) == 0) + throw new Exception("Transaction could not be created in database."); + } - if (!skipBucketAssignment) + if (!skipBucketAssignment) + { + // Create new bucket assignments + foreach (var bucket in Buckets) { - // Create new bucket assignments - foreach (var bucket in Buckets) + var newBudgetedTransaction = new BudgetedTransaction { - var newBudgetedTransaction = new BudgetedTransaction - { - TransactionId = Transaction.TransactionId, - BucketId = bucket.SelectedBucket.BucketId, - Amount = bucket.Amount - }; - // Execute DB Update - dbContext.CreateBudgetedTransaction(newBudgetedTransaction); - } + TransactionId = Transaction.TransactionId, + BucketId = bucket.SelectedBucket.BucketId, + Amount = bucket.Amount + }; + // Execute DB Update + dbContext.CreateBudgetedTransaction(newBudgetedTransaction); } - - transaction.Commit(); - return new ViewModelOperationResult(true); - } - catch (Exception e) - { - transaction.Rollback(); - return new ViewModelOperationResult(false, $"Errors during database update: {e.Message}"); } + + transaction.Commit(); + return new ViewModelOperationResult(true); } - } - } + catch (Exception e) + { + transaction.Rollback(); + return new ViewModelOperationResult(false, $"Errors during database update: {e.Message}"); + } + } + } + } - /// - /// Executes several data consistency checks (e.g. Bucket assignment, pending amount etc.) to see if changes - /// can be stored in the database - /// - /// Exclude checks on Bucket assignment - /// Object which contains information and results of this method - private ViewModelOperationResult PerformConsistencyCheck(out bool skipBucketAssignment) + /// + /// Executes several data consistency checks (e.g. Bucket assignment, pending amount etc.) to see if changes + /// can be stored in the database + /// + /// Exclude checks on Bucket assignment + /// Object which contains information and results of this method + private ViewModelOperationResult PerformConsistencyCheck(out bool skipBucketAssignment) + { + decimal assignedAmount = 0; + skipBucketAssignment = false; + + // Consistency and Validity Checks + if (Transaction == null) return new ViewModelOperationResult(false, "Errors in Transaction object."); + if (SelectedAccount == null || SelectedAccount.AccountId == 0) return new ViewModelOperationResult(false, "No Bank account selected."); + if (Buckets.Count == 0) return new ViewModelOperationResult(false, "No Bucket assigned to this Transaction."); + + foreach (var assignedBucket in Buckets) { - decimal assignedAmount = 0; - skipBucketAssignment = false; - - // Consistency and Validity Checks - if (Transaction == null) return new ViewModelOperationResult(false, "Errors in Transaction object."); - if (SelectedAccount == null || SelectedAccount.AccountId == 0) return new ViewModelOperationResult(false, "No Bank account selected."); - if (Buckets.Count == 0) return new ViewModelOperationResult(false, "No Bucket assigned to this Transaction."); - - foreach (var assignedBucket in Buckets) - { - if (assignedBucket.SelectedBucket == null) - return new ViewModelOperationResult(false, "Pending Bucket assignment for this Transaction."); + if (assignedBucket.SelectedBucket == null) + return new ViewModelOperationResult(false, "Pending Bucket assignment for this Transaction."); - if (assignedBucket.SelectedBucket.BucketId == 0) + if (assignedBucket.SelectedBucket.BucketId == 0) + { + if (assignedBucket.SelectedBucket.Name == "No Selection") { - if (assignedBucket.SelectedBucket.Name == "No Selection") - { - // Imported Transaction where Bucket assignment is pending - // Allow Transaction Update but Skip DB Updates for Bucket assignment - skipBucketAssignment = true; - } - else - { - return new ViewModelOperationResult(false, "Pending Bucket assignment for this Transaction."); - } + // Imported Transaction where Bucket assignment is pending + // Allow Transaction Update but Skip DB Updates for Bucket assignment + skipBucketAssignment = true; + } + else + { + return new ViewModelOperationResult(false, "Pending Bucket assignment for this Transaction."); } - - assignedAmount += assignedBucket.Amount; } - if (assignedAmount != Transaction.Amount) return new ViewModelOperationResult(false, "Amount between Bucket assignment and Transaction not consistent."); - - return new ViewModelOperationResult(true); + assignedAmount += assignedBucket.Amount; } - /// - /// Removes a record in the database based on object - /// - /// Removes also all its assigned Buckets - /// Object which contains information and results of this method - private ViewModelOperationResult DeleteTransaction() + if (assignedAmount != Transaction.Amount) return new ViewModelOperationResult(false, "Amount between Bucket assignment and Transaction not consistent."); + + return new ViewModelOperationResult(true); + } + + /// + /// Removes a record in the database based on object + /// + /// Removes also all its assigned Buckets + /// Object which contains information and results of this method + private ViewModelOperationResult DeleteTransaction() + { + using (var dbContext = new DatabaseContext(_dbOptions)) { - using (var dbContext = new DatabaseContext(_dbOptions)) + using (var transaction = dbContext.Database.BeginTransaction()) { - using (var transaction = dbContext.Database.BeginTransaction()) + try { - try - { - // Delete BankTransaction in DB - dbContext.DeleteBankTransaction(Transaction); + // Delete BankTransaction in DB + dbContext.DeleteBankTransaction(Transaction); - // Delete all previous bucket assignments for transaction - var budgetedTransactions = dbContext.BudgetedTransaction - .Where(i => i.TransactionId == Transaction.TransactionId); - dbContext.DeleteBudgetedTransactions(budgetedTransactions); + // Delete all previous bucket assignments for transaction + var budgetedTransactions = dbContext.BudgetedTransaction + .Where(i => i.TransactionId == Transaction.TransactionId); + dbContext.DeleteBudgetedTransactions(budgetedTransactions); - transaction.Commit(); - return new ViewModelOperationResult(true); - } - catch (Exception e) - { - transaction.Rollback(); - return new ViewModelOperationResult(false, $"Errors during database update: {e.Message}"); - } + transaction.Commit(); + return new ViewModelOperationResult(true); } - } - } - - public void StartModification() - { - _oldTransactionViewModelItem = new TransactionViewModelItem(_dbOptions, _yearMonthViewModel, Transaction); - InModification = true; - } - - public void CancelModification() - { - Transaction = _oldTransactionViewModelItem.Transaction; - SelectedAccount = _oldTransactionViewModelItem.SelectedAccount; - Buckets = _oldTransactionViewModelItem.Buckets; - InModification = false; - _oldTransactionViewModelItem = null; - } + catch (Exception e) + { + transaction.Rollback(); + return new ViewModelOperationResult(false, $"Errors during database update: {e.Message}"); + } + } + } + } - public ViewModelOperationResult CreateItem() - { - Transaction.TransactionId = 0; // Triggers CREATE during CreateOrUpdateTransaction() - return CreateOrUpdateTransaction(); - } + public void StartModification() + { + _oldTransactionViewModelItem = new TransactionViewModelItem(_dbOptions, _yearMonthViewModel, Transaction); + InModification = true; + } - public ViewModelOperationResult UpdateItem() - { - if (Transaction.TransactionId < 1) return new ViewModelOperationResult(false, "Transaction needs to be created first in database"); + public void CancelModification() + { + Transaction = _oldTransactionViewModelItem.Transaction; + SelectedAccount = _oldTransactionViewModelItem.SelectedAccount; + Buckets = _oldTransactionViewModelItem.Buckets; + InModification = false; + _oldTransactionViewModelItem = null; + } - var result = CreateOrUpdateTransaction(); - if (!result.IsSuccessful) - { - return new ViewModelOperationResult(false, result.Message, true); - } - _oldTransactionViewModelItem = null; - InModification = false; + public ViewModelOperationResult CreateItem() + { + Transaction.TransactionId = 0; // Triggers CREATE during CreateOrUpdateTransaction() + return CreateOrUpdateTransaction(); + } - return new ViewModelOperationResult(true, false); - } + public ViewModelOperationResult UpdateItem() + { + if (Transaction.TransactionId < 1) return new ViewModelOperationResult(false, "Transaction needs to be created first in database"); - public ViewModelOperationResult DeleteItem() + var result = CreateOrUpdateTransaction(); + if (!result.IsSuccessful) { - var result = DeleteTransaction(); - if (!result.IsSuccessful) return result; - - return new ViewModelOperationResult(true, true); + return new ViewModelOperationResult(false, result.Message, true); } + _oldTransactionViewModelItem = null; + InModification = false; - public void ProposeBucket() - { - var proposal = CheckMappingRules(); - if (proposal == null) return; - Buckets.Clear(); - Buckets.Add(new PartialBucketViewModelItem(_dbOptions, _yearMonthViewModel.CurrentMonth, proposal, Transaction.Amount)); - } + return new ViewModelOperationResult(true, false); + } + + public ViewModelOperationResult DeleteItem() + { + var result = DeleteTransaction(); + if (!result.IsSuccessful) return result; + + return new ViewModelOperationResult(true, true); + } + + public void ProposeBucket() + { + var proposal = CheckMappingRules(); + if (proposal == null) return; + Buckets.Clear(); + Buckets.Add(new PartialBucketViewModelItem(_dbOptions, _yearMonthViewModel.CurrentMonth, proposal, Transaction.Amount)); + } - private Bucket CheckMappingRules() + private Bucket CheckMappingRules() + { + var targetBucketId = 0; + using (var dbContext = new DatabaseContext(_dbOptions)) { - var targetBucketId = 0; - using (var dbContext = new DatabaseContext(_dbOptions)) + foreach (var ruleSet in dbContext.BucketRuleSet.OrderBy(i => i.Priority)) { - foreach (var ruleSet in dbContext.BucketRuleSet.OrderBy(i => i.Priority)) + using (var mappingRuleDbContext = new DatabaseContext(_dbOptions)) { - using (var mappingRuleDbContext = new DatabaseContext(_dbOptions)) + if (mappingRuleDbContext.MappingRule + .Where(i => i.BucketRuleSetId == ruleSet.BucketRuleSetId) + .All(DoesRuleApply)) { - if (mappingRuleDbContext.MappingRule - .Where(i => i.BucketRuleSetId == ruleSet.BucketRuleSetId) - .All(DoesRuleApply)) - { - targetBucketId = ruleSet.TargetBucketId; - break; - } + targetBucketId = ruleSet.TargetBucketId; + break; } } - - return targetBucketId != 0 ? dbContext.Bucket.First(i => i.BucketId == targetBucketId) : null; } - bool DoesRuleApply(MappingRule mappingRule) - { - var cleanedComparisionValue = mappingRule.ComparisionValue.ToLower(); - switch (mappingRule.ComparisionType) - { - case 1: - return cleanedComparisionValue == GetFieldValue(mappingRule.ComparisionField); - case 2: - return cleanedComparisionValue != GetFieldValue(mappingRule.ComparisionField); - case 3: - return GetFieldValue(mappingRule.ComparisionField).Contains(cleanedComparisionValue); - case 4: - return !GetFieldValue(mappingRule.ComparisionField).Contains(cleanedComparisionValue); - } + return targetBucketId != 0 ? dbContext.Bucket.First(i => i.BucketId == targetBucketId) : null; + } - return false; + bool DoesRuleApply(MappingRule mappingRule) + { + var cleanedComparisionValue = mappingRule.ComparisionValue.ToLower(); + switch (mappingRule.ComparisionType) + { + case 1: + return cleanedComparisionValue == GetFieldValue(mappingRule.ComparisionField); + case 2: + return cleanedComparisionValue != GetFieldValue(mappingRule.ComparisionField); + case 3: + return GetFieldValue(mappingRule.ComparisionField).Contains(cleanedComparisionValue); + case 4: + return !GetFieldValue(mappingRule.ComparisionField).Contains(cleanedComparisionValue); } - string GetFieldValue(int comparisionField) - { - string result; - switch (comparisionField) - { - case 1: - result = Transaction.AccountId.ToString(); - break; - case 2: - result = Transaction.Payee; - break; - case 3: - result = Transaction.Memo; - break; - case 4: - result = Transaction.Amount.ToString(); - break; - default: - result = null; - break; - } + return false; + } - return result == null ? string.Empty : result.ToLower(); + string GetFieldValue(int comparisionField) + { + string result; + switch (comparisionField) + { + case 1: + result = Transaction.AccountId.ToString(); + break; + case 2: + result = Transaction.Payee; + break; + case 3: + result = Transaction.Memo; + break; + case 4: + result = Transaction.Amount.ToString(); + break; + default: + result = null; + break; } + + return result == null ? string.Empty : result.ToLower(); } } } diff --git a/OpenBudgeteer.Core/ViewModels/ReportViewModel.cs b/OpenBudgeteer.Core/ViewModels/ReportViewModel.cs index aaced8f..94cc8b9 100644 --- a/OpenBudgeteer.Core/ViewModels/ReportViewModel.cs +++ b/OpenBudgeteer.Core/ViewModels/ReportViewModel.cs @@ -1,302 +1,296 @@ using System; using System.Collections.Generic; -using System.Collections.ObjectModel; -using System.Globalization; using System.Linq; -using System.Text; using System.Threading.Tasks; using Microsoft.EntityFrameworkCore; -using Microsoft.EntityFrameworkCore.Migrations.Operations; using OpenBudgeteer.Core.Common.Database; using OpenBudgeteer.Core.Models; using OpenBudgeteer.Core.ViewModels.ItemViewModels; -namespace OpenBudgeteer.Core.ViewModels +namespace OpenBudgeteer.Core.ViewModels; + +public class ReportViewModel : ViewModelBase { - public class ReportViewModel : ViewModelBase + private readonly DbContextOptions _dbOptions; + + /// + /// Basic constructor + /// + /// Options to connect to a database + public ReportViewModel(DbContextOptions dbOptions) { - private readonly DbContextOptions _dbOptions; + _dbOptions = dbOptions; + } - /// - /// Basic constructor - /// - /// Options to connect to a database - public ReportViewModel(DbContextOptions dbOptions) + /// + /// Loads a set of balances per month from the database + /// + /// Considers only within a month + /// Number of months that should be loaded + /// + /// Collection of containing + /// Item1: representing the month + /// Item2: representing the balance + /// + public async Task>> LoadMonthBalancesAsync(int months = 24) + { + return await Task.Run(() => { - _dbOptions = dbOptions; - } + var result = new List>(); + var currentMonth = new DateTime(DateTime.Now.Year, DateTime.Now.Month, 1); - /// - /// Loads a set of balances per month from the database - /// - /// Considers only within a month - /// Number of months that should be loaded - /// - /// Collection of containing - /// Item1: representing the month - /// Item2: representing the balance - /// - public async Task>> LoadMonthBalancesAsync(int months = 24) - { - return await Task.Run(() => + using (var dbContext = new DatabaseContext(_dbOptions)) { - var result = new List>(); - var currentMonth = new DateTime(DateTime.Now.Year, DateTime.Now.Month, 1); + var transactions = dbContext.BankTransaction + .Where(i => i.TransactionDate >= currentMonth.AddMonths(months * -1)) + .OrderBy(i => i.TransactionDate) + .ToList(); + var monthBalances = transactions + .GroupBy(i => new DateTime(i.TransactionDate.Year, i.TransactionDate.Month, 1)) + .Select(i => new + { + YearMonth = i.Key, + Balance = i.Sum(j => j.Amount) + }); - using (var dbContext = new DatabaseContext(_dbOptions)) + foreach (var group in monthBalances) { - var transactions = dbContext.BankTransaction - .Where(i => i.TransactionDate >= currentMonth.AddMonths(months * -1)) - .OrderBy(i => i.TransactionDate) - .ToList(); - var monthBalances = transactions - .GroupBy(i => new DateTime(i.TransactionDate.Year, i.TransactionDate.Month, 1)) - .Select(i => new - { - YearMonth = i.Key, - Balance = i.Sum(j => j.Amount) - }); - - foreach (var group in monthBalances) - { - result.Add(new Tuple(group.YearMonth, group.Balance)); - } + result.Add(new Tuple(group.YearMonth, group.Balance)); } + } - return result; - }); - } + return result; + }); + } - /// - /// Loads a set of income and expenses per month from the database - /// - /// Number of months that should be loaded - /// - /// Collection of containing - /// Item1: representing the month - /// Item2: representing the income - /// Item3: representing the expenses - /// - public async Task>> LoadMonthIncomeExpensesAsync(int months = 24) + /// + /// Loads a set of income and expenses per month from the database + /// + /// Number of months that should be loaded + /// + /// Collection of containing + /// Item1: representing the month + /// Item2: representing the income + /// Item3: representing the expenses + /// + public async Task>> LoadMonthIncomeExpensesAsync(int months = 24) + { + return await Task.Run(() => { - return await Task.Run(() => - { - var result = new List>(); - var currentMonth = new DateTime(DateTime.Now.Year, DateTime.Now.Month, 1); - - using (var dbContext = new DatabaseContext(_dbOptions)) - { - // Get all Transactions which are not marked as "Transfer" - var transactions = dbContext.BankTransaction - .Join( - dbContext.BudgetedTransaction, - bankTransaction => bankTransaction.TransactionId, - budgetedTransaction => budgetedTransaction.TransactionId, - (bankTransaction, budgetedTransaction) => new - { - bankTransaction.TransactionId, - bankTransaction.TransactionDate, - budgetedTransaction.Amount, - budgetedTransaction.BucketId - }) - .Where(i => - i.BucketId != 2 && - i.TransactionDate >= currentMonth.AddMonths(months * -1)) - .ToList(); + var result = new List>(); + var currentMonth = new DateTime(DateTime.Now.Year, DateTime.Now.Month, 1); - var monthIncomeExpenses = transactions - .GroupBy(i => new DateTime(i.TransactionDate.Year, i.TransactionDate.Month, 1)) - .Select(i => new + using (var dbContext = new DatabaseContext(_dbOptions)) + { + // Get all Transactions which are not marked as "Transfer" + var transactions = dbContext.BankTransaction + .Join( + dbContext.BudgetedTransaction, + bankTransaction => bankTransaction.TransactionId, + budgetedTransaction => budgetedTransaction.TransactionId, + (bankTransaction, budgetedTransaction) => new { - YearMonth = i.Key, - Income = i.Where(j => j.Amount > 0).Sum(j => j.Amount), - Expenses = (i.Where(j => j.Amount < 0).Sum(j => j.Amount)) * -1 + bankTransaction.TransactionId, + bankTransaction.TransactionDate, + budgetedTransaction.Amount, + budgetedTransaction.BucketId }) - .OrderBy(i => i.YearMonth); + .Where(i => + i.BucketId != 2 && + i.TransactionDate >= currentMonth.AddMonths(months * -1)) + .ToList(); - foreach (var group in monthIncomeExpenses) + var monthIncomeExpenses = transactions + .GroupBy(i => new DateTime(i.TransactionDate.Year, i.TransactionDate.Month, 1)) + .Select(i => new { - result.Add(new Tuple(group.YearMonth, group.Income, group.Expenses)); - } + YearMonth = i.Key, + Income = i.Where(j => j.Amount > 0).Sum(j => j.Amount), + Expenses = (i.Where(j => j.Amount < 0).Sum(j => j.Amount)) * -1 + }) + .OrderBy(i => i.YearMonth); + + foreach (var group in monthIncomeExpenses) + { + result.Add(new Tuple(group.YearMonth, group.Income, group.Expenses)); } + } - return result; - }); - } + return result; + }); + } - /// - /// Loads a set of income and expenses per year from the database - /// - /// Number of years that should be loaded - /// Collection of containing - /// Item1: representing the year - /// Item2: representing the income - /// Item3: representing the expenses - /// - public async Task>> LoadYearIncomeExpensesAsync(int years = 5) + /// + /// Loads a set of income and expenses per year from the database + /// + /// Number of years that should be loaded + /// Collection of containing + /// Item1: representing the year + /// Item2: representing the income + /// Item3: representing the expenses + /// + public async Task>> LoadYearIncomeExpensesAsync(int years = 5) + { + return await Task.Run(() => { - return await Task.Run(() => - { - var result = new List>(); - var currentMonth = new DateTime(DateTime.Now.Year, DateTime.Now.Month, 1); + var result = new List>(); + var currentMonth = new DateTime(DateTime.Now.Year, DateTime.Now.Month, 1); - using (var dbContext = new DatabaseContext(_dbOptions)) - { - // Get all Transactions which are not marked as "Transfer" - var transactions = dbContext.BankTransaction - .Join( - dbContext.BudgetedTransaction, - bankTransaction => bankTransaction.TransactionId, - budgetedTransaction => budgetedTransaction.TransactionId, - (bankTransaction, budgetedTransaction) => new - { - bankTransaction.TransactionId, - bankTransaction.TransactionDate, - budgetedTransaction.Amount, - budgetedTransaction.BucketId - }) - .Where(i => - i.BucketId != 2 && - i.TransactionDate >= currentMonth.AddYears(years * -1)) - .ToList(); - - var yearIncomeExpenses = transactions - .GroupBy(i => new DateTime(i.TransactionDate.Year, 1, 1)) - .Select(i => new + using (var dbContext = new DatabaseContext(_dbOptions)) + { + // Get all Transactions which are not marked as "Transfer" + var transactions = dbContext.BankTransaction + .Join( + dbContext.BudgetedTransaction, + bankTransaction => bankTransaction.TransactionId, + budgetedTransaction => budgetedTransaction.TransactionId, + (bankTransaction, budgetedTransaction) => new { - Year = i.Key, - Income = i.Where(j => j.Amount > 0).Sum(j => j.Amount), - Expenses = (i.Where(j => j.Amount < 0).Sum(j => j.Amount)) * -1 + bankTransaction.TransactionId, + bankTransaction.TransactionDate, + budgetedTransaction.Amount, + budgetedTransaction.BucketId }) - .OrderBy(i => i.Year); + .Where(i => + i.BucketId != 2 && + i.TransactionDate >= currentMonth.AddYears(years * -1)) + .ToList(); - foreach (var group in yearIncomeExpenses) + var yearIncomeExpenses = transactions + .GroupBy(i => new DateTime(i.TransactionDate.Year, 1, 1)) + .Select(i => new { - result.Add(new Tuple(group.Year, group.Income, group.Expenses)); - } + Year = i.Key, + Income = i.Where(j => j.Amount > 0).Sum(j => j.Amount), + Expenses = (i.Where(j => j.Amount < 0).Sum(j => j.Amount)) * -1 + }) + .OrderBy(i => i.Year); + + foreach (var group in yearIncomeExpenses) + { + result.Add(new Tuple(group.Year, group.Income, group.Expenses)); } + } - return result; - }); - } + return result; + }); + } - /// - /// Loads a set of balances per month from the database showing the progress of the overall bank balance - /// - /// Considers all from the past - /// Number of months that should be loaded - /// - /// Collection of containing - /// Item1: representing the month - /// Item2: representing the balance - /// - public async Task>> LoadBankBalancesAsync(int months = 24) + /// + /// Loads a set of balances per month from the database showing the progress of the overall bank balance + /// + /// Considers all from the past + /// Number of months that should be loaded + /// + /// Collection of containing + /// Item1: representing the month + /// Item2: representing the balance + /// + public async Task>> LoadBankBalancesAsync(int months = 24) + { + return await Task.Run(() => { - return await Task.Run(() => - { - var result = new List>(); - var currentMonth = new DateTime(DateTime.Now.Year, DateTime.Now.Month, 1); + var result = new List>(); + var currentMonth = new DateTime(DateTime.Now.Year, DateTime.Now.Month, 1); - using (var dbContext = new DatabaseContext(_dbOptions)) + using (var dbContext = new DatabaseContext(_dbOptions)) + { + for (int monthIndex = months; monthIndex >= 0; monthIndex--) { - for (int monthIndex = months; monthIndex >= 0; monthIndex--) - { - var month = currentMonth.AddMonths(monthIndex * -1); - var bankBalance = dbContext.BankTransaction - .Where(i => i.TransactionDate < month.AddMonths(1)) - .OrderBy(i => i.TransactionDate) - .Sum(i => i.Amount); - result.Add(new Tuple(month, bankBalance)); - } + var month = currentMonth.AddMonths(monthIndex * -1); + var bankBalance = dbContext.BankTransaction + .Where(i => i.TransactionDate < month.AddMonths(1)) + .OrderBy(i => i.TransactionDate) + .Sum(i => i.Amount); + result.Add(new Tuple(month, bankBalance)); } + } - return result; - }); - } + return result; + }); + } - /// - /// Loads a set of expenses of a per month from the database - /// - /// Number of months that should be loaded - /// - /// Collection of ViewModelItems containing information about a and its expenses per month - /// - public async Task> LoadMonthExpensesBucketAsync(int month = 12) + /// + /// Loads a set of expenses of a per month from the database + /// + /// Number of months that should be loaded + /// + /// Collection of ViewModelItems containing information about a and its expenses per month + /// + public async Task> LoadMonthExpensesBucketAsync(int month = 12) + { + return await Task.Run(() => { - return await Task.Run(() => - { - var currentMonth = new DateTime(DateTime.Now.Year, DateTime.Now.Month, 1).AddMonths(1); - var result = new List(); + var currentMonth = new DateTime(DateTime.Now.Year, DateTime.Now.Month, 1).AddMonths(1); + var result = new List(); - using (var dbContext = new DatabaseContext(_dbOptions)) + using (var dbContext = new DatabaseContext(_dbOptions)) + { + var buckets = dbContext.Bucket + .Where(i => !i.IsInactive && i.BucketId > 2) + .OrderBy(i => i.Name); + foreach (var bucket in buckets) { - var buckets = dbContext.Bucket - .Where(i => !i.IsInactive && i.BucketId > 2) - .OrderBy(i => i.Name); - foreach (var bucket in buckets) + // Get latest BucketVersion based on passed parameter + using (var bucketVersionDbContext = new DatabaseContext(_dbOptions)) { - // Get latest BucketVersion based on passed parameter - using (var bucketVersionDbContext = new DatabaseContext(_dbOptions)) + var newReportRecord = new MonthlyBucketExpensesReportViewModelItem(); + var latestVersion = bucketVersionDbContext.BucketVersion + .Where(i => i.BucketId == bucket.BucketId) + .ToList() + .OrderByDescending(i => i.Version) + .First(); + if (latestVersion.BucketType != 2) continue; + using (var budgetedTransactionDbContext = new DatabaseContext(_dbOptions)) { - var newReportRecord = new MonthlyBucketExpensesReportViewModelItem(); - var latestVersion = bucketVersionDbContext.BucketVersion - .Where(i => i.BucketId == bucket.BucketId) - .ToList() - .OrderByDescending(i => i.Version) - .First(); - if (latestVersion.BucketType != 2) continue; - using (var budgetedTransactionDbContext = new DatabaseContext(_dbOptions)) - { - var queryResults = budgetedTransactionDbContext.BankTransaction - // Join with BudgetedTransaction - .Join(budgetedTransactionDbContext.BudgetedTransaction, - transaction => transaction.TransactionId, - budgetedTransaction => budgetedTransaction.TransactionId, - ((transaction, budgetedTransaction) => new - { - Transaction = transaction, - BudgetedTransaction = budgetedTransaction - })) - // Limit on Transactions for the current Bucket and the last x months - .Where(i => i.BudgetedTransaction.BucketId == bucket.BucketId && - i.Transaction.TransactionDate >= currentMonth.AddMonths(month * -1)) - // Group the results per YearMonth - .GroupBy(i => new DateTime(i.Transaction.TransactionDate.Year, i.Transaction.TransactionDate.Month, 1)) - // Create a new Grouped Object - .Select(i => new + var queryResults = budgetedTransactionDbContext.BankTransaction + // Join with BudgetedTransaction + .Join(budgetedTransactionDbContext.BudgetedTransaction, + transaction => transaction.TransactionId, + budgetedTransaction => budgetedTransaction.TransactionId, + ((transaction, budgetedTransaction) => new { - YearMonth = i.Key, - Balance = (i.Sum(j => j.Transaction.Amount)) * -1 - }) - .ToList(); + Transaction = transaction, + BudgetedTransaction = budgetedTransaction + })) + // Limit on Transactions for the current Bucket and the last x months + .Where(i => i.BudgetedTransaction.BucketId == bucket.BucketId && + i.Transaction.TransactionDate >= currentMonth.AddMonths(month * -1)) + // Group the results per YearMonth + .GroupBy(i => new DateTime(i.Transaction.TransactionDate.Year, i.Transaction.TransactionDate.Month, 1)) + // Create a new Grouped Object + .Select(i => new + { + YearMonth = i.Key, + Balance = (i.Sum(j => j.Transaction.Amount)) * -1 + }) + .ToList(); - // Collect results - if (queryResults.Count == 0) continue; // No data available. Nothing to add - newReportRecord.BucketName = bucket.Name; - var reportInsertMonth = queryResults.First().YearMonth; - foreach (var queryResult in queryResults) + // Collect results + if (queryResults.Count == 0) continue; // No data available. Nothing to add + newReportRecord.BucketName = bucket.Name; + var reportInsertMonth = queryResults.First().YearMonth; + foreach (var queryResult in queryResults) + { + // Create empty MonthlyResults in case no data for specific months are available + while (queryResult.YearMonth != reportInsertMonth) { - // Create empty MonthlyResults in case no data for specific months are available - while (queryResult.YearMonth != reportInsertMonth) - { - newReportRecord.MonthlyResults.Add(new Tuple( - reportInsertMonth, - 0)); - reportInsertMonth = reportInsertMonth.AddMonths(1); - } newReportRecord.MonthlyResults.Add(new Tuple( - queryResult.YearMonth, - queryResult.Balance)); + reportInsertMonth, + 0)); reportInsertMonth = reportInsertMonth.AddMonths(1); } + newReportRecord.MonthlyResults.Add(new Tuple( + queryResult.YearMonth, + queryResult.Balance)); + reportInsertMonth = reportInsertMonth.AddMonths(1); } - result.Add(newReportRecord); } + result.Add(newReportRecord); } } - return result; - }); - } + } + return result; + }); } } - diff --git a/OpenBudgeteer.Core/ViewModels/RulesViewModel.cs b/OpenBudgeteer.Core/ViewModels/RulesViewModel.cs index a434291..7836c7e 100644 --- a/OpenBudgeteer.Core/ViewModels/RulesViewModel.cs +++ b/OpenBudgeteer.Core/ViewModels/RulesViewModel.cs @@ -1,194 +1,190 @@ using System; -using System.Collections.Generic; using System.Collections.ObjectModel; using System.Linq; -using System.Text; using System.Threading.Tasks; using Microsoft.EntityFrameworkCore; using OpenBudgeteer.Core.Common; using OpenBudgeteer.Core.Common.Database; -using OpenBudgeteer.Core.Common.EventClasses; using OpenBudgeteer.Core.Models; using OpenBudgeteer.Core.ViewModels.ItemViewModels; -namespace OpenBudgeteer.Core.ViewModels +namespace OpenBudgeteer.Core.ViewModels; + +public class RulesViewModel : ViewModelBase { - public class RulesViewModel : ViewModelBase + private RuleSetViewModelItem _newRuleSet; + /// + /// Helper property to handle setup of a new + /// + public RuleSetViewModelItem NewRuleSet { - private RuleSetViewModelItem _newRuleSet; - /// - /// Helper property to handle setup of a new - /// - public RuleSetViewModelItem NewRuleSet - { - get => _newRuleSet; - set => Set(ref _newRuleSet, value); - } + get => _newRuleSet; + set => Set(ref _newRuleSet, value); + } - private ObservableCollection _ruleSets; - /// - /// Collection of all from the database - /// - public ObservableCollection RuleSets - { - get => _ruleSets; - set => Set(ref _ruleSets, value); - } + private ObservableCollection _ruleSets; + /// + /// Collection of all from the database + /// + public ObservableCollection RuleSets + { + get => _ruleSets; + set => Set(ref _ruleSets, value); + } - private readonly DbContextOptions _dbOptions; + private readonly DbContextOptions _dbOptions; - /// - /// Basic constructor - /// - /// Options to connect to a database - public RulesViewModel(DbContextOptions dbOptions) - { - _dbOptions = dbOptions; - RuleSets = new ObservableCollection(); - ResetNewRuleSet(); - } - - /// - /// Initialize ViewModel and load data from database - /// - /// Object which contains information and results of this method - public async Task LoadDataAsync() + /// + /// Basic constructor + /// + /// Options to connect to a database + public RulesViewModel(DbContextOptions dbOptions) + { + _dbOptions = dbOptions; + RuleSets = new ObservableCollection(); + ResetNewRuleSet(); + } + + /// + /// Initialize ViewModel and load data from database + /// + /// Object which contains information and results of this method + public async Task LoadDataAsync() + { + return await Task.Run(() => { - return await Task.Run(() => + try { - try + RuleSets.Clear(); + using (var dbContext = new DatabaseContext(_dbOptions)) { - RuleSets.Clear(); - using (var dbContext = new DatabaseContext(_dbOptions)) + foreach (var bucketRuleSet in dbContext.BucketRuleSet.OrderBy(i => i.Priority)) { - foreach (var bucketRuleSet in dbContext.BucketRuleSet.OrderBy(i => i.Priority)) - { - RuleSets.Add(new RuleSetViewModelItem(_dbOptions, bucketRuleSet)); - } + RuleSets.Add(new RuleSetViewModelItem(_dbOptions, bucketRuleSet)); } - - return new ViewModelOperationResult(true); - } - catch (Exception e) - { - return new ViewModelOperationResult(false, $"Error during loading: {e.Message}"); } - }); - } - /// - /// Starts creation process based on - /// - /// Triggers - /// Object which contains information and results of this method - public ViewModelOperationResult CreateNewRuleSet() - { - NewRuleSet.RuleSet.BucketRuleSetId = 0; - var result = NewRuleSet.CreateUpdateRuleSetItem(); - if (!result.IsSuccessful) return result; - ResetNewRuleSet(); + return new ViewModelOperationResult(true); + } + catch (Exception e) + { + return new ViewModelOperationResult(false, $"Error during loading: {e.Message}"); + } + }); + } - return new ViewModelOperationResult(true, true); - } + /// + /// Starts creation process based on + /// + /// Triggers + /// Object which contains information and results of this method + public ViewModelOperationResult CreateNewRuleSet() + { + NewRuleSet.RuleSet.BucketRuleSetId = 0; + var result = NewRuleSet.CreateUpdateRuleSetItem(); + if (!result.IsSuccessful) return result; + ResetNewRuleSet(); - /// - /// Helper method to reset values of - /// - public void ResetNewRuleSet() - { - NewRuleSet = new RuleSetViewModelItem(_dbOptions); - // Defaults required because if initial selection in UI will not be updated by User - // then binding will not update these properties - NewRuleSet.MappingRules.Add(new MappingRuleViewModelItem(_dbOptions, new MappingRule() - { - ComparisionField = 1, - ComparisionType = 1 - })); - } + return new ViewModelOperationResult(true, true); + } - /// - /// Starts Creation or Update process for the passed - /// - /// Updates collection - /// Instance that needs to be created or updated - /// Object which contains information and results of this method - public ViewModelOperationResult SaveRuleSetItem(RuleSetViewModelItem ruleSet) + /// + /// Helper method to reset values of + /// + public void ResetNewRuleSet() + { + NewRuleSet = new RuleSetViewModelItem(_dbOptions); + // Defaults required because if initial selection in UI will not be updated by User + // then binding will not update these properties + NewRuleSet.MappingRules.Add(new MappingRuleViewModelItem(_dbOptions, new MappingRule() { - var result = ruleSet.CreateUpdateRuleSetItem(); - if (!result.IsSuccessful) return result; - RuleSets = new ObservableCollection(RuleSets.OrderBy(i => i.RuleSet.Priority)); + ComparisionField = 1, + ComparisionType = 1 + })); + } - return result; - } + /// + /// Starts Creation or Update process for the passed + /// + /// Updates collection + /// Instance that needs to be created or updated + /// Object which contains information and results of this method + public ViewModelOperationResult SaveRuleSetItem(RuleSetViewModelItem ruleSet) + { + var result = ruleSet.CreateUpdateRuleSetItem(); + if (!result.IsSuccessful) return result; + RuleSets = new ObservableCollection(RuleSets.OrderBy(i => i.RuleSet.Priority)); + + return result; + } - /// - /// Starts Deletion process for the passed including all its - /// - /// Updates collection - /// Instance that needs to be deleted - /// Object which contains information and results of this method - public ViewModelOperationResult DeleteRuleSetItem(RuleSetViewModelItem ruleSet) + /// + /// Starts Deletion process for the passed including all its + /// + /// Updates collection + /// Instance that needs to be deleted + /// Object which contains information and results of this method + public ViewModelOperationResult DeleteRuleSetItem(RuleSetViewModelItem ruleSet) + { + using (var dbContext = new DatabaseContext(_dbOptions)) { - using (var dbContext = new DatabaseContext(_dbOptions)) + using (var dbTransaction = dbContext.Database.BeginTransaction()) { - using (var dbTransaction = dbContext.Database.BeginTransaction()) + try { - try - { - dbContext.DeleteMappingRules(dbContext.MappingRule - .Where(i => i.BucketRuleSetId == ruleSet.RuleSet.BucketRuleSetId)); - dbContext.DeleteBucketRuleSet(ruleSet.RuleSet); - dbTransaction.Commit(); - RuleSets.Remove(ruleSet); + dbContext.DeleteMappingRules(dbContext.MappingRule + .Where(i => i.BucketRuleSetId == ruleSet.RuleSet.BucketRuleSetId)); + dbContext.DeleteBucketRuleSet(ruleSet.RuleSet); + dbTransaction.Commit(); + RuleSets.Remove(ruleSet); - return new ViewModelOperationResult(true); - } - catch (Exception e) - { - dbTransaction.Rollback(); - return new ViewModelOperationResult(false, $"Errors during database update: {e.Message}"); - } + return new ViewModelOperationResult(true); + } + catch (Exception e) + { + dbTransaction.Rollback(); + return new ViewModelOperationResult(false, $"Errors during database update: {e.Message}"); } } } + } - /// - /// Helper method to start Modification process for all - /// - public void EditAllRules() + /// + /// Helper method to start Modification process for all + /// + public void EditAllRules() + { + foreach (var ruleSet in RuleSets) { - foreach (var ruleSet in RuleSets) - { - ruleSet.StartModification(); - } + ruleSet.StartModification(); } + } - /// - /// Starts the Creation or Update process for all - /// - /// Updates collection - /// Object which contains information and results of this method - public ViewModelOperationResult SaveAllRules() + /// + /// Starts the Creation or Update process for all + /// + /// Updates collection + /// Object which contains information and results of this method + public ViewModelOperationResult SaveAllRules() + { + using (var dbTransaction = new DatabaseContext(_dbOptions).Database.BeginTransaction()) { - using (var dbTransaction = new DatabaseContext(_dbOptions).Database.BeginTransaction()) + try { - try + foreach (var ruleSet in RuleSets) { - foreach (var ruleSet in RuleSets) - { - var result = ruleSet.CreateUpdateRuleSetItem(); - if (!result.IsSuccessful) throw new Exception(result.Message); - } - dbTransaction.Commit(); - RuleSets = new ObservableCollection(RuleSets.OrderBy(i => i.RuleSet.Priority)); - - return new ViewModelOperationResult(true); - } - catch (Exception e) - { - dbTransaction.Rollback(); - return new ViewModelOperationResult(false, e.Message); + var result = ruleSet.CreateUpdateRuleSetItem(); + if (!result.IsSuccessful) throw new Exception(result.Message); } + dbTransaction.Commit(); + RuleSets = new ObservableCollection(RuleSets.OrderBy(i => i.RuleSet.Priority)); + + return new ViewModelOperationResult(true); + } + catch (Exception e) + { + dbTransaction.Rollback(); + return new ViewModelOperationResult(false, e.Message); } } } diff --git a/OpenBudgeteer.Core/ViewModels/TransactionViewModel.cs b/OpenBudgeteer.Core/ViewModels/TransactionViewModel.cs index eaf78e5..a102ce7 100644 --- a/OpenBudgeteer.Core/ViewModels/TransactionViewModel.cs +++ b/OpenBudgeteer.Core/ViewModels/TransactionViewModel.cs @@ -3,379 +3,374 @@ using System; using System.Collections.Generic; using System.Collections.ObjectModel; -using System.ComponentModel; using System.Linq; -using System.Text; -using System.Text.RegularExpressions; using System.Threading.Tasks; -using System.Windows; using OpenBudgeteer.Core.ViewModels.ItemViewModels; using Microsoft.EntityFrameworkCore; using OpenBudgeteer.Core.Common; using OpenBudgeteer.Core.Common.EventClasses; using OpenBudgeteer.Core.Common.Extensions; -namespace OpenBudgeteer.Core.ViewModels +namespace OpenBudgeteer.Core.ViewModels; + +/// +/// Identifier which kind of filter can be applied on the +/// +public enum TransactionViewModelFilter: int +{ + [StringValue("No Filter")] + NoFilter = 0, + [StringValue("Hide mapped")] + HideMapped = 1, + [StringValue("Only mapped")] + OnlyMapped = 2, + [StringValue("In Modification")] + InModification = 3, +} + +public class TransactionViewModel : ViewModelBase { + private TransactionViewModelItem _newTransaction; /// - /// Identifier which kind of filter can be applied on the + /// Helper property to handle creation of a new /// - public enum TransactionViewModelFilter: int + public TransactionViewModelItem NewTransaction { - [StringValue("No Filter")] - NoFilter = 0, - [StringValue("Hide mapped")] - HideMapped = 1, - [StringValue("Only mapped")] - OnlyMapped = 2, - [StringValue("In Modification")] - InModification = 3, + get => _newTransaction; + set => Set(ref _newTransaction, value); } - public class TransactionViewModel : ViewModelBase + private int _proposeBucketsCount; + /// + /// Helper property for Progress Dialog during Bucket proposal process + /// + public int ProposeBucketsCount { - private TransactionViewModelItem _newTransaction; - /// - /// Helper property to handle creation of a new - /// - public TransactionViewModelItem NewTransaction - { - get => _newTransaction; - set => Set(ref _newTransaction, value); - } - - private int _proposeBucketsCount; - /// - /// Helper property for Progress Dialog during Bucket proposal process - /// - public int ProposeBucketsCount - { - get => _proposeBucketsCount; - set => Set(ref _proposeBucketsCount, value); - } + get => _proposeBucketsCount; + set => Set(ref _proposeBucketsCount, value); + } - private int _proposeBucketsProgress; - /// - /// Helper property for Progress Dialog during Bucket proposal process - /// - public int ProposeBucketsProgress - { - get => _proposeBucketsProgress; - set => Set(ref _proposeBucketsProgress, value); - } + private int _proposeBucketsProgress; + /// + /// Helper property for Progress Dialog during Bucket proposal process + /// + public int ProposeBucketsProgress + { + get => _proposeBucketsProgress; + set => Set(ref _proposeBucketsProgress, value); + } - private int _proposeBucketsPercentage; - /// - /// Helper property for Progress Dialog during Bucket proposal process - /// - public int ProposeBucketsPercentage + private int _proposeBucketsPercentage; + /// + /// Helper property for Progress Dialog during Bucket proposal process + /// + public int ProposeBucketsPercentage + { + get => _proposeBucketsPercentage; + set => Set(ref _proposeBucketsPercentage, value); + } + + private TransactionViewModelFilter _currentFilter; + /// + /// Sets the current filter for the ViewModel + /// + public TransactionViewModelFilter CurrentFilter + { + get => _currentFilter; + set { - get => _proposeBucketsPercentage; - set => Set(ref _proposeBucketsPercentage, value); - } - - private TransactionViewModelFilter _currentFilter; - /// - /// Sets the current filter for the ViewModel - /// - public TransactionViewModelFilter CurrentFilter + if (Set(ref _currentFilter, value)) + NotifyPropertyChanged(nameof(Transactions)); + } + } + + private ObservableCollection _transactions; + /// + /// Collection of loaded Transactions + /// + public ObservableCollection Transactions + { + get { - get => _currentFilter; - set + switch (CurrentFilter) { - if (Set(ref _currentFilter, value)) - NotifyPropertyChanged(nameof(Transactions)); - } + case TransactionViewModelFilter.HideMapped: + return new ObservableCollection( + _transactions.Where(i => i.Buckets.First().SelectedBucket.BucketId == 0)); + case TransactionViewModelFilter.OnlyMapped: + return new ObservableCollection( + _transactions.Where(i => i.Buckets.First().SelectedBucket.BucketId > 0)); + case TransactionViewModelFilter.InModification: + return new ObservableCollection( + _transactions.Where(i => i.InModification)); + case TransactionViewModelFilter.NoFilter: + default: + return _transactions; + } } + protected set => Set(ref _transactions, value); + } + + /// + /// EventHandler which should be invoked during Bucket Proposal to track overall Progress + /// + public event EventHandler BucketProposalProgressChanged; + + private readonly DbContextOptions _dbOptions; + private readonly YearMonthSelectorViewModel _yearMonthViewModel; + + /// + /// Basic Constructor + /// + /// Options to connect to a database + /// ViewModel instance to handle selection of a year and month + public TransactionViewModel(DbContextOptions dbOptions, YearMonthSelectorViewModel yearMonthViewModel) + { + _dbOptions = dbOptions; + _yearMonthViewModel = yearMonthViewModel; + ResetNewTransaction(); + _transactions = new ObservableCollection(); + //_yearMonthViewModel.SelectedYearMonthChanged += (sender) => { LoadData(); }; + } - private ObservableCollection _transactions; - /// - /// Collection of loaded Transactions - /// - public ObservableCollection Transactions + /// + /// Initialize ViewModel and load data from database + /// + /// Object which contains information and results of this method + public async Task LoadDataAsync() + { + try { - get + // Get all available transactions. The TransactionViewModelItem takes care to find all assigned buckets for + // each passed transaction. It creates also the respective ViewModelObjects + _transactions.Clear(); + + using (var dbContext = new DatabaseContext(_dbOptions)) { - switch (CurrentFilter) + var sql = $"SELECT * FROM {nameof(BankTransaction)} " + + $"WHERE {nameof(BankTransaction.TransactionDate)} LIKE '{_yearMonthViewModel.CurrentMonth:yyyy-MM}%' " + + $"ORDER BY {nameof(BankTransaction.TransactionDate)}"; + var transactions = dbContext.BankTransaction.FromSqlRaw(sql); + + var transactionTasks = new List>(); + + foreach (var transaction in transactions) { - case TransactionViewModelFilter.HideMapped: - return new ObservableCollection( - _transactions.Where(i => i.Buckets.First().SelectedBucket.BucketId == 0)); - case TransactionViewModelFilter.OnlyMapped: - return new ObservableCollection( - _transactions.Where(i => i.Buckets.First().SelectedBucket.BucketId > 0)); - case TransactionViewModelFilter.InModification: - return new ObservableCollection( - _transactions.Where(i => i.InModification)); - case TransactionViewModelFilter.NoFilter: - default: - return _transactions; + transactionTasks.Add(TransactionViewModelItem.CreateAsync(_dbOptions, _yearMonthViewModel, transaction)); } + + foreach (var transaction in await Task.WhenAll(transactionTasks)) + { + _transactions.Add(transaction); + } + + return new ViewModelOperationResult(true); } - protected set => Set(ref _transactions, value); } - - /// - /// EventHandler which should be invoked during Bucket Proposal to track overall Progress - /// - public event EventHandler BucketProposalProgressChanged; - - private readonly DbContextOptions _dbOptions; - private readonly YearMonthSelectorViewModel _yearMonthViewModel; - - /// - /// Basic Constructor - /// - /// Options to connect to a database - /// ViewModel instance to handle selection of a year and month - public TransactionViewModel(DbContextOptions dbOptions, YearMonthSelectorViewModel yearMonthViewModel) + catch (Exception e) { - _dbOptions = dbOptions; - _yearMonthViewModel = yearMonthViewModel; - ResetNewTransaction(); - _transactions = new ObservableCollection(); - //_yearMonthViewModel.SelectedYearMonthChanged += (sender) => { LoadData(); }; + return new ViewModelOperationResult(false, $"Error during loading: {e.Message}"); } + } - /// - /// Initialize ViewModel and load data from database - /// - /// Object which contains information and results of this method - public async Task LoadDataAsync() + /// + /// Initialize ViewModel and load data from database but only for assigned to the + /// passed . Optionally will be transformed to + /// + /// Bucket for which Transactions should be loaded + /// Include which will be transformed to + /// Object which contains information and results of this method + public async Task LoadDataAsync(Bucket bucket, bool withMovements) + { + try { - try - { - // Get all available transactions. The TransactionViewModelItem takes care to find all assigned buckets for - // each passed transaction. It creates also the respective ViewModelObjects - _transactions.Clear(); + _transactions.Clear(); - using (var dbContext = new DatabaseContext(_dbOptions)) - { - var sql = $"SELECT * FROM {nameof(BankTransaction)} " + - $"WHERE {nameof(BankTransaction.TransactionDate)} LIKE '{_yearMonthViewModel.CurrentMonth:yyyy-MM}%' " + - $"ORDER BY {nameof(BankTransaction.TransactionDate)}"; - var transactions = dbContext.BankTransaction.FromSqlRaw(sql); + using (var dbContext = new DatabaseContext(_dbOptions)) + { + var transactionTasks = new List>(); - var transactionTasks = new List>(); + // Get all BankTransaction + var results = dbContext.BankTransaction + .Join( + dbContext.BudgetedTransaction, + bankTransaction => bankTransaction.TransactionId, + budgetedTransaction => budgetedTransaction.TransactionId, + (bankTransaction, budgetedTransaction) => new + { + BankTransaction = bankTransaction, + BudgetedTransaction = budgetedTransaction + }) + .Where(i => i.BudgetedTransaction.BucketId == bucket.BucketId) + .OrderByDescending(i => i.BankTransaction.TransactionDate) + .ToList(); - foreach (var transaction in transactions) - { - transactionTasks.Add(TransactionViewModelItem.CreateAsync(_dbOptions, _yearMonthViewModel, transaction)); - } + foreach (var result in results) + { + transactionTasks.Add(TransactionViewModelItem.CreateWithoutBucketsAsync(_dbOptions, _yearMonthViewModel, result.BankTransaction)); + } - foreach (var transaction in await Task.WhenAll(transactionTasks)) + if (withMovements) + { + // Get Bucket Movements + var bucketMovements = dbContext.BucketMovement + .Where(i => i.BucketId == bucket.BucketId) + .ToList(); + foreach (var bucketMovement in bucketMovements) { - _transactions.Add(transaction); + transactionTasks.Add(TransactionViewModelItem.CreateFromBucketMovementAsync(bucketMovement)); } + } - return new ViewModelOperationResult(true); + foreach (var transaction in (await Task.WhenAll(transactionTasks)) + .OrderByDescending(i => i.Transaction.TransactionDate)) + { + _transactions.Add(transaction); } + + return new ViewModelOperationResult(true); } - catch (Exception e) - { - return new ViewModelOperationResult(false, $"Error during loading: {e.Message}"); - } } + catch (Exception e) + { + return new ViewModelOperationResult(false, $"Error during loading: {e.Message}"); + } + } - /// - /// Initialize ViewModel and load data from database but only for assigned to the - /// passed . Optionally will be transformed to - /// - /// Bucket for which Transactions should be loaded - /// Include which will be transformed to - /// Object which contains information and results of this method - public async Task LoadDataAsync(Bucket bucket, bool withMovements) + /// + /// Initialize ViewModel and load data from database but only for assigned to the + /// passed + /// + /// Account for which Transactions should be loaded + /// Object which contains information and results of this method + public async Task LoadDataAsync(Account account) + { + try { - try + _transactions.Clear(); + using (var dbContext = new DatabaseContext(_dbOptions)) { - _transactions.Clear(); - - using (var dbContext = new DatabaseContext(_dbOptions)) - { - var transactionTasks = new List>(); - - // Get all BankTransaction - var results = dbContext.BankTransaction - .Join( - dbContext.BudgetedTransaction, - bankTransaction => bankTransaction.TransactionId, - budgetedTransaction => budgetedTransaction.TransactionId, - (bankTransaction, budgetedTransaction) => new - { - BankTransaction = bankTransaction, - BudgetedTransaction = budgetedTransaction - }) - .Where(i => i.BudgetedTransaction.BucketId == bucket.BucketId) - .OrderByDescending(i => i.BankTransaction.TransactionDate) + var results = + dbContext.BankTransaction + .Where(i => i.AccountId == account.AccountId) + .OrderByDescending(i => i.TransactionDate) .ToList(); - foreach (var result in results) - { - transactionTasks.Add(TransactionViewModelItem.CreateWithoutBucketsAsync(_dbOptions, _yearMonthViewModel, result.BankTransaction)); - } - - if (withMovements) - { - // Get Bucket Movements - var bucketMovements = dbContext.BucketMovement - .Where(i => i.BucketId == bucket.BucketId) - .ToList(); - foreach (var bucketMovement in bucketMovements) - { - transactionTasks.Add(TransactionViewModelItem.CreateFromBucketMovementAsync(bucketMovement)); - } - } - - foreach (var transaction in (await Task.WhenAll(transactionTasks)) - .OrderByDescending(i => i.Transaction.TransactionDate)) - { - _transactions.Add(transaction); - } - - return new ViewModelOperationResult(true); + var transactions = results.Count < 100 ? results : results.GetRange(0, 100); + var transactionTasks = new List>(); + foreach (var transaction in transactions) + { + transactionTasks.Add(TransactionViewModelItem.CreateWithoutBucketsAsync(_dbOptions, _yearMonthViewModel, transaction)); } - } - catch (Exception e) - { - return new ViewModelOperationResult(false, $"Error during loading: {e.Message}"); - } - } - /// - /// Initialize ViewModel and load data from database but only for assigned to the - /// passed - /// - /// Account for which Transactions should be loaded - /// Object which contains information and results of this method - public async Task LoadDataAsync(Account account) - { - try - { - _transactions.Clear(); - using (var dbContext = new DatabaseContext(_dbOptions)) + foreach (var transaction in await Task.WhenAll(transactionTasks)) { - var results = - dbContext.BankTransaction - .Where(i => i.AccountId == account.AccountId) - .OrderByDescending(i => i.TransactionDate) - .ToList(); - - var transactions = results.Count < 100 ? results : results.GetRange(0, 100); - var transactionTasks = new List>(); - foreach (var transaction in transactions) - { - transactionTasks.Add(TransactionViewModelItem.CreateWithoutBucketsAsync(_dbOptions, _yearMonthViewModel, transaction)); - } - - foreach (var transaction in await Task.WhenAll(transactionTasks)) - { - _transactions.Add(transaction); - } - - return new ViewModelOperationResult(true); + _transactions.Add(transaction); } - } - catch (Exception e) - { - return new ViewModelOperationResult(false, $"Error during loading: {e.Message}"); + + return new ViewModelOperationResult(true); } } - - /// - /// Starts creation process based on - /// - /// Triggers - /// Object which contains information and results of this method - public ViewModelOperationResult CreateItem() + catch (Exception e) { - NewTransaction.Transaction.TransactionId = 0; - var result = NewTransaction.CreateItem(); - if (!result.IsSuccessful) return result; - ResetNewTransaction(); - - return new ViewModelOperationResult(true, true); + return new ViewModelOperationResult(false, $"Error during loading: {e.Message}"); } + } - /// - /// Helper method to reset values of - /// - public void ResetNewTransaction() - { - NewTransaction = new TransactionViewModelItem(_dbOptions, _yearMonthViewModel); - NewTransaction.Buckets.Add(new PartialBucketViewModelItem(_dbOptions, _yearMonthViewModel.CurrentMonth)); - } + /// + /// Starts creation process based on + /// + /// Triggers + /// Object which contains information and results of this method + public ViewModelOperationResult CreateItem() + { + NewTransaction.Transaction.TransactionId = 0; + var result = NewTransaction.CreateItem(); + if (!result.IsSuccessful) return result; + ResetNewTransaction(); + + return new ViewModelOperationResult(true, true); + } - /// - /// Helper method to start modification process for all Transactions based on current Filter - /// - public void EditAllTransaction() - { - foreach (var transaction in Transactions) - { - transaction.StartModification(); - } + /// + /// Helper method to reset values of + /// + public void ResetNewTransaction() + { + NewTransaction = new TransactionViewModelItem(_dbOptions, _yearMonthViewModel); + NewTransaction.Buckets.Add(new PartialBucketViewModelItem(_dbOptions, _yearMonthViewModel.CurrentMonth)); + } - CurrentFilter = TransactionViewModelFilter.InModification; + /// + /// Helper method to start modification process for all Transactions based on current Filter + /// + public void EditAllTransaction() + { + foreach (var transaction in Transactions) + { + transaction.StartModification(); } - /// - /// Starts update process for all Transactions - /// - /// Object which contains information and results of this method - public ViewModelOperationResult SaveAllTransaction() + CurrentFilter = TransactionViewModelFilter.InModification; + } + + /// + /// Starts update process for all Transactions + /// + /// Object which contains information and results of this method + public ViewModelOperationResult SaveAllTransaction() + { + using (var dbTransaction = new DatabaseContext(_dbOptions).Database.BeginTransaction()) { - using (var dbTransaction = new DatabaseContext(_dbOptions).Database.BeginTransaction()) + try { - try - { - foreach (var transaction in _transactions.Where(i => i.InModification)) - { - var result = transaction.UpdateItem(); - if (!result.IsSuccessful) throw new Exception(result.Message); - } - dbTransaction.Commit(); - CurrentFilter = TransactionViewModelFilter.NoFilter; - return new ViewModelOperationResult(true); - } - catch (Exception e) + foreach (var transaction in _transactions.Where(i => i.InModification)) { - dbTransaction.Rollback(); - return new ViewModelOperationResult(false, e.Message); + var result = transaction.UpdateItem(); + if (!result.IsSuccessful) throw new Exception(result.Message); } + dbTransaction.Commit(); + CurrentFilter = TransactionViewModelFilter.NoFilter; + return new ViewModelOperationResult(true); + } + catch (Exception e) + { + dbTransaction.Rollback(); + return new ViewModelOperationResult(false, e.Message); } } + } - /// - /// Cancels update process for all Transactions. Reloads ViewModel to restore data. - /// - /// Object which contains information and results of this method - public async Task CancelAllTransactionAsync() - { - CurrentFilter = TransactionViewModelFilter.NoFilter; - return await LoadDataAsync(); - } + /// + /// Cancels update process for all Transactions. Reloads ViewModel to restore data. + /// + /// Object which contains information and results of this method + public async Task CancelAllTransactionAsync() + { + CurrentFilter = TransactionViewModelFilter.NoFilter; + return await LoadDataAsync(); + } - /// - /// Starts process to propose the right for all Transactions - /// - /// Sets all Transactions into Modification Mode in case they have a "No Selection" Bucket - public void ProposeBuckets() - { - CurrentFilter = TransactionViewModelFilter.InModification; - var unassignedTransactions = _transactions.Where(i => i.Buckets.First().SelectedBucket.BucketId == 0); - ProposeBucketsCount = unassignedTransactions.Count(); - ProposeBucketsProgress = 0; + /// + /// Starts process to propose the right for all Transactions + /// + /// Sets all Transactions into Modification Mode in case they have a "No Selection" Bucket + public void ProposeBuckets() + { + CurrentFilter = TransactionViewModelFilter.InModification; + var unassignedTransactions = _transactions.Where(i => i.Buckets.First().SelectedBucket.BucketId == 0); + ProposeBucketsCount = unassignedTransactions.Count(); + ProposeBucketsProgress = 0; - foreach (var transaction in unassignedTransactions) - { - transaction.StartModification(); - transaction.ProposeBucket(); - ProposeBucketsProgress++; - ProposeBucketsPercentage = ProposeBucketsCount == 0 ? 0 : - Convert.ToInt32(Decimal.Divide(ProposeBucketsProgress, ProposeBucketsCount) * 100); - BucketProposalProgressChanged?.Invoke(this, - new ProposeBucketChangedEventArgs(ProposeBucketsProgress, ProposeBucketsPercentage)); - } + foreach (var transaction in unassignedTransactions) + { + transaction.StartModification(); + transaction.ProposeBucket(); + ProposeBucketsProgress++; + ProposeBucketsPercentage = ProposeBucketsCount == 0 ? 0 : + Convert.ToInt32(Decimal.Divide(ProposeBucketsProgress, ProposeBucketsCount) * 100); + BucketProposalProgressChanged?.Invoke(this, + new ProposeBucketChangedEventArgs(ProposeBucketsProgress, ProposeBucketsPercentage)); } } } diff --git a/OpenBudgeteer.Core/ViewModels/ViewModelBase.cs b/OpenBudgeteer.Core/ViewModels/ViewModelBase.cs index acb3a61..cea7ce5 100644 --- a/OpenBudgeteer.Core/ViewModels/ViewModelBase.cs +++ b/OpenBudgeteer.Core/ViewModels/ViewModelBase.cs @@ -1,30 +1,26 @@ -using System; -using System.Collections.Generic; +using System.Collections.Generic; using System.ComponentModel; -using System.Text; using System.Runtime.CompilerServices; -namespace OpenBudgeteer.Core.ViewModels +namespace OpenBudgeteer.Core.ViewModels; + +public class ViewModelBase : INotifyPropertyChanged { - public class ViewModelBase : INotifyPropertyChanged + protected bool Set(ref T field, T value, [CallerMemberName] string propertyName = "") { - protected bool Set(ref T field, T value, [CallerMemberName] string propertyName = "") + if (EqualityComparer.Default.Equals(field, value)) { - if (EqualityComparer.Default.Equals(field, value)) - { - return false; - } - field = value; - NotifyPropertyChanged(propertyName); - return true; + return false; } + field = value; + NotifyPropertyChanged(propertyName); + return true; + } - public event PropertyChangedEventHandler PropertyChanged; + public event PropertyChangedEventHandler PropertyChanged; - protected void NotifyPropertyChanged(string propertyName) - { - PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName)); - } + protected void NotifyPropertyChanged(string propertyName) + { + PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName)); } } - diff --git a/OpenBudgeteer.Core/ViewModels/YearMonthSelectorViewModel.cs b/OpenBudgeteer.Core/ViewModels/YearMonthSelectorViewModel.cs index d804f52..ac04f67 100644 --- a/OpenBudgeteer.Core/ViewModels/YearMonthSelectorViewModel.cs +++ b/OpenBudgeteer.Core/ViewModels/YearMonthSelectorViewModel.cs @@ -1,110 +1,107 @@ using System; -using System.Collections.Generic; using System.Collections.ObjectModel; -using System.Text; using OpenBudgeteer.Core.Common.EventClasses; -namespace OpenBudgeteer.Core.ViewModels +namespace OpenBudgeteer.Core.ViewModels; + +public class YearMonthSelectorViewModel : ViewModelBase { - public class YearMonthSelectorViewModel : ViewModelBase + private int _selectedMonth; + /// + /// Number of the current month + /// + /// Change triggers + public int SelectedMonth { - private int _selectedMonth; - /// - /// Number of the current month - /// - /// Change triggers - public int SelectedMonth + get => _selectedMonth; + set { - get => _selectedMonth; - set - { - var valueChanged = Set(ref _selectedMonth, value); - if (!_yearMontIsChanging && valueChanged) SelectedYearMonthChanged?.Invoke(this, new ViewModelReloadEventArgs(this)); - } + var valueChanged = Set(ref _selectedMonth, value); + if (!_yearMontIsChanging && valueChanged) SelectedYearMonthChanged?.Invoke(this, new ViewModelReloadEventArgs(this)); } + } - private int _selectedYear; - /// - /// Number of the current year - /// - /// Change triggers - public int SelectedYear + private int _selectedYear; + /// + /// Number of the current year + /// + /// Change triggers + public int SelectedYear + { + get => _selectedYear; + set { - get => _selectedYear; - set - { - var valueChanged = Set(ref _selectedYear, value); - if (!_yearMontIsChanging && valueChanged) SelectedYearMonthChanged?.Invoke(this, new ViewModelReloadEventArgs(this)); - } + var valueChanged = Set(ref _selectedYear, value); + if (!_yearMontIsChanging && valueChanged) SelectedYearMonthChanged?.Invoke(this, new ViewModelReloadEventArgs(this)); } + } - private ObservableCollection _months; - /// - /// Helper collection which contains the number of all months - /// - public ObservableCollection Months - { - get => _months; - private set => Set(ref _months, value); - } + private ObservableCollection _months; + /// + /// Helper collection which contains the number of all months + /// + public ObservableCollection Months + { + get => _months; + private set => Set(ref _months, value); + } - /// - /// Returns the first day as based on and - /// - public DateTime CurrentMonth => new DateTime(SelectedYear, SelectedMonth, 1); + /// + /// Returns the first day as based on and + /// + public DateTime CurrentMonth => new DateTime(SelectedYear, SelectedMonth, 1); - /// - /// EventHandler which should be invoked once the a year and/or a month has been modified. To be used to trigger - /// ViewModel reloads which are dependent on this ViewModel - /// - public event EventHandler SelectedYearMonthChanged; + /// + /// EventHandler which should be invoked once the a year and/or a month has been modified. To be used to trigger + /// ViewModel reloads which are dependent on this ViewModel + /// + public event EventHandler SelectedYearMonthChanged; - private bool _yearMontIsChanging; // prevents double invoke of SelectedYearMonthChanged - - /// - /// Basic constructor - /// - public YearMonthSelectorViewModel() + private bool _yearMontIsChanging; // prevents double invoke of SelectedYearMonthChanged + + /// + /// Basic constructor + /// + public YearMonthSelectorViewModel() + { + Months = new ObservableCollection(); + for (var i = 1; i < 13; i++) { - Months = new ObservableCollection(); - for (var i = 1; i < 13; i++) - { - Months.Add(i); - } - SelectedMonth = DateTime.Now.Month; - SelectedYear = DateTime.Now.Year; + Months.Add(i); } + SelectedMonth = DateTime.Now.Month; + SelectedYear = DateTime.Now.Year; + } - /// - /// Moves to the previous month - /// - /// Triggers - public void PreviousMonth() - { - UpdateYearMonth(CurrentMonth.AddMonths(-1)); - } + /// + /// Moves to the previous month + /// + /// Triggers + public void PreviousMonth() + { + UpdateYearMonth(CurrentMonth.AddMonths(-1)); + } - /// - /// Moves to the next month - /// - /// Triggers - public void NextMonth() - { - UpdateYearMonth(CurrentMonth.AddMonths(1)); - } + /// + /// Moves to the next month + /// + /// Triggers + public void NextMonth() + { + UpdateYearMonth(CurrentMonth.AddMonths(1)); + } - /// - /// Sets the date to the passed - /// - /// New date - /// Triggers (only once) - private void UpdateYearMonth(DateTime newYearMonth) - { - _yearMontIsChanging = true; - SelectedYear = newYearMonth.Year; - SelectedMonth = newYearMonth.Month; - _yearMontIsChanging = false; - SelectedYearMonthChanged?.Invoke(this, new ViewModelReloadEventArgs(this)); - } + /// + /// Sets the date to the passed + /// + /// New date + /// Triggers (only once) + private void UpdateYearMonth(DateTime newYearMonth) + { + _yearMontIsChanging = true; + SelectedYear = newYearMonth.Year; + SelectedMonth = newYearMonth.Month; + _yearMontIsChanging = false; + SelectedYearMonthChanged?.Invoke(this, new ViewModelReloadEventArgs(this)); } } From 434dc8522fac856a9a8da47eba851b0bc1634ecd Mon Sep 17 00:00:00 2001 From: Alexander Preibisch Date: Fri, 12 Nov 2021 16:40:46 +0100 Subject: [PATCH 31/50] Update CHANGELOG.md --- CHANGELOG.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 248b37d..5a78791 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,7 +2,7 @@ * [Add] Dialog for showing progress during Bucket proposal #21 * [Add] Filter on Transaction Page #25 -* [Changed] Core and Blazor Frontend updated to .Net 5.0 +* [Changed] Core and Blazor Frontend updated to .Net 6.0 * [Changed] File Preview on Import Page now read-only * [Changed] Misc small visual updates and fixes on Import Page * [Changed] Consistent Chart Header styles on Report Page From d0a324618901a87f95bdbda7e720ed743e8463c3 Mon Sep 17 00:00:00 2001 From: Alexander Preibisch Date: Fri, 12 Nov 2021 16:54:59 +0100 Subject: [PATCH 32/50] Update Change Log link to new Github repo #46 --- OpenBudgeteer.Blazor/Shared/NavMenu.razor | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/OpenBudgeteer.Blazor/Shared/NavMenu.razor b/OpenBudgeteer.Blazor/Shared/NavMenu.razor index 65cfb46..cfb8f57 100644 --- a/OpenBudgeteer.Blazor/Shared/NavMenu.razor +++ b/OpenBudgeteer.Blazor/Shared/NavMenu.razor @@ -54,7 +54,7 @@ From c2b866e2602deb31934d48a16dcddb745fd7622e Mon Sep 17 00:00:00 2001 From: Alexander Preibisch Date: Fri, 12 Nov 2021 16:59:19 +0100 Subject: [PATCH 33/50] Update links on Home Page to new Github repository #47 --- OpenBudgeteer.Blazor/Pages/Index.razor | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/OpenBudgeteer.Blazor/Pages/Index.razor b/OpenBudgeteer.Blazor/Pages/Index.razor index 2c32d75..de76a08 100644 --- a/OpenBudgeteer.Blazor/Pages/Index.razor +++ b/OpenBudgeteer.Blazor/Pages/Index.razor @@ -20,8 +20,8 @@
-
OpenBudgeteer Repository
-

Access source code hosted on Gitlab

+
OpenBudgeteer Repository
+

Access source code hosted on Github

@@ -33,8 +33,8 @@
-
Create Issue
-

Found a bug or missing a feature? Create an issue on Gitlab

+
Create Issue
+

Found a bug or missing a feature? Create an issue on Github

@@ -46,7 +46,7 @@
-
Display Change Log
+
Display Change Log

See latest updates and changes.

From f4c3c718e3f600224e0a596523f71449b531949f Mon Sep 17 00:00:00 2001 From: Alexander Preibisch Date: Fri, 12 Nov 2021 17:01:34 +0100 Subject: [PATCH 34/50] Fixed text in confirmation message box for deleting a Rule #44 --- OpenBudgeteer.Blazor/Pages/Rules.razor | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/OpenBudgeteer.Blazor/Pages/Rules.razor b/OpenBudgeteer.Blazor/Pages/Rules.razor index f60c0ea..8444ed7 100644 --- a/OpenBudgeteer.Blazor/Pages/Rules.razor +++ b/OpenBudgeteer.Blazor/Pages/Rules.razor @@ -223,8 +223,8 @@ From e17415f0dbc2b3f10f7138c39e2af10458bf0a4b Mon Sep 17 00:00:00 2001 From: Alexander Preibisch Date: Fri, 12 Nov 2021 17:03:44 +0100 Subject: [PATCH 35/50] Update CHANGELOG.md --- CHANGELOG.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 5a78791..a34c401 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,12 +9,14 @@ * [Changed] Updated dependencies. Thanks @kekkon * [Changed] Simplified dependency tree. Thanks @kekkon * [Changed] Progress calculation for several scenarios #26 #28 +* [Changed] Links and text due to swtich to Github #46 #47 * [Fixed] Reworked UI update handling to fix issues on refreshing data #22 * [Fixed] Missing Target Account update for newly created or updated Import Profiles #23 * [Fixed] `MonthOutputConverter.Convert` not using Culture. Thanks @kekkon * [Fixed] `OpenBudgeteer.Core.Test.ViewModelTest.YearMonthSelectorViewModelTest.Constructor_CheckDefaults` test using thread culture. Thanks @kekkon * [Fixed] Added Validation checks before saving Bucket data to fix DivideByZeroException #29 * [Fixed] Trigger of `SelectedYearMonthChanged` passing `OpenBudgeteer.Core.Test.ViewModelTest.SelectedYearMonthChanged_CheckEventHasBeenInvoked` Test +* [Fixed] Wrong text in confirmation message box for deleting a Rule #44 ### 1.3 (2020-12-15) From c4ca7409f0dc1da0d076f3c99b5556fdc0910a3d Mon Sep 17 00:00:00 2001 From: Alexander Preibisch Date: Fri, 12 Nov 2021 19:20:57 +0100 Subject: [PATCH 36/50] Updated Tewr.Blazor.FileReader to fix error during file read --- OpenBudgeteer.Blazor/OpenBudgeteer.Blazor.csproj | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/OpenBudgeteer.Blazor/OpenBudgeteer.Blazor.csproj b/OpenBudgeteer.Blazor/OpenBudgeteer.Blazor.csproj index dc005c9..5da1e82 100644 --- a/OpenBudgeteer.Blazor/OpenBudgeteer.Blazor.csproj +++ b/OpenBudgeteer.Blazor/OpenBudgeteer.Blazor.csproj @@ -11,7 +11,7 @@ - +
From 3185ada478036f687f330bb74edd3266c73aac35 Mon Sep 17 00:00:00 2001 From: Alexander Preibisch Date: Fri, 12 Nov 2021 19:43:33 +0100 Subject: [PATCH 37/50] On Import Page final message box shows an option to clear the form #45 --- CHANGELOG.md | 1 + OpenBudgeteer.Blazor/Pages/Import.razor | 15 ++++++++++++++- 2 files changed, 15 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index a34c401..6a0fb2e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,7 @@ * [Add] Dialog for showing progress during Bucket proposal #21 * [Add] Filter on Transaction Page #25 +* [Add] On Import Page final message box shows an option to clear the form #45 * [Changed] Core and Blazor Frontend updated to .Net 6.0 * [Changed] File Preview on Import Page now read-only * [Changed] Misc small visual updates and fixes on Import Page diff --git a/OpenBudgeteer.Blazor/Pages/Import.razor b/OpenBudgeteer.Blazor/Pages/Import.razor index f5bff8a..1080aa6 100644 --- a/OpenBudgeteer.Blazor/Pages/Import.razor +++ b/OpenBudgeteer.Blazor/Pages/Import.razor @@ -291,7 +291,8 @@ @_importConfirmationMessage @@ -418,6 +419,18 @@ _isConfirmationModalDialogVisible = true; } + async Task ClearFormAsync() + { + _isConfirmationModalDialogVisible = false; + _step2Enabled = false; + _step3Enabled = false; + _step4Enabled = false; + _selectedFileName = "Choose File"; + await FileReaderService.CreateReference(_inputElement).ClearValue(); + _dataContext = new ImportDataViewModel(DbContextOptions); + HandleResult((_dataContext.LoadData())); + } + void ImportProfile_SelectionChanged(ChangeEventArgs e) { _step3Enabled = false; From c71969c97ca0f505a95bf997250ba2015fa710ce Mon Sep 17 00:00:00 2001 From: Alexander Preibisch Date: Fri, 12 Nov 2021 19:50:55 +0100 Subject: [PATCH 38/50] Upgrade Nuget Packages --- OpenBudgeteer.Blazor/OpenBudgeteer.Blazor.csproj | 2 +- OpenBudgeteer.Core.Test/OpenBudgeteer.Core.Test.csproj | 4 ++-- OpenBudgeteer.Core/OpenBudgeteer.Core.csproj | 6 +++--- 3 files changed, 6 insertions(+), 6 deletions(-) diff --git a/OpenBudgeteer.Blazor/OpenBudgeteer.Blazor.csproj b/OpenBudgeteer.Blazor/OpenBudgeteer.Blazor.csproj index 5da1e82..8c663b1 100644 --- a/OpenBudgeteer.Blazor/OpenBudgeteer.Blazor.csproj +++ b/OpenBudgeteer.Blazor/OpenBudgeteer.Blazor.csproj @@ -9,7 +9,7 @@ - + diff --git a/OpenBudgeteer.Core.Test/OpenBudgeteer.Core.Test.csproj b/OpenBudgeteer.Core.Test/OpenBudgeteer.Core.Test.csproj index a026f00..5166efd 100644 --- a/OpenBudgeteer.Core.Test/OpenBudgeteer.Core.Test.csproj +++ b/OpenBudgeteer.Core.Test/OpenBudgeteer.Core.Test.csproj @@ -10,8 +10,8 @@ - - + + all diff --git a/OpenBudgeteer.Core/OpenBudgeteer.Core.csproj b/OpenBudgeteer.Core/OpenBudgeteer.Core.csproj index e844703..bc4494c 100644 --- a/OpenBudgeteer.Core/OpenBudgeteer.Core.csproj +++ b/OpenBudgeteer.Core/OpenBudgeteer.Core.csproj @@ -5,12 +5,12 @@ - - + + all runtime; build; native; contentfiles; analyzers; buildtransitive - + From 715dd4405740440fc60fb81201eb2508c19d1d77 Mon Sep 17 00:00:00 2001 From: Alexander Preibisch Date: Fri, 12 Nov 2021 21:37:44 +0100 Subject: [PATCH 39/50] Reworked ProposeBucketsInfoDialog as it was not working properly #21 --- CHANGELOG.md | 2 +- OpenBudgeteer.Blazor/Pages/Transaction.razor | 49 +++++++++-------- .../Shared/ProcessingProgressDialog.razor | 52 ------------------- .../ViewModels/TransactionViewModel.cs | 25 +++++---- 4 files changed, 39 insertions(+), 89 deletions(-) delete mode 100644 OpenBudgeteer.Blazor/Shared/ProcessingProgressDialog.razor diff --git a/CHANGELOG.md b/CHANGELOG.md index 6a0fb2e..7b1574c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,6 +1,6 @@ ### 1.4 (20xx-xx-xx) -* [Add] Dialog for showing progress during Bucket proposal #21 +* [Add] Info Dialog during Bucket proposal and optimized proposal performance #21 * [Add] Filter on Transaction Page #25 * [Add] On Import Page final message box shows an option to clear the form #45 * [Changed] Core and Blazor Frontend updated to .Net 6.0 diff --git a/OpenBudgeteer.Blazor/Pages/Transaction.razor b/OpenBudgeteer.Blazor/Pages/Transaction.razor index e3fd825..282bf06 100644 --- a/OpenBudgeteer.Blazor/Pages/Transaction.razor +++ b/OpenBudgeteer.Blazor/Pages/Transaction.razor @@ -11,7 +11,6 @@ @inject DbContextOptions DbContextOptions @inject YearMonthSelectorViewModel YearMonthDataContext -
@if (_massEditEnabled) @@ -23,7 +22,7 @@ { - + }
@@ -221,6 +220,22 @@ +@if (_isProposeBucketsInfoDialogVisible) +{ + + +} + - - @code { TransactionViewModel _dataContext; bool _newTransactionEnabled; @@ -249,7 +260,7 @@ bool _isErrorModalDialogVisible; string _errorModalDialogMessage; - ProcessingProgressDialog _proposeBucketsProcessingProgressDialog; + bool _isProposeBucketsInfoDialogVisible; protected override async Task OnInitializedAsync() { @@ -276,27 +287,15 @@ _dataContext.EditAllTransaction(); } - void ProposeBuckets() + async Task ProposeBucketsAsync() { - _proposeBucketsProcessingProgressDialog.Message = "Propose Buckets for unassigned Transactions..."; - _proposeBucketsProcessingProgressDialog.MinValue = 0; - _proposeBucketsProcessingProgressDialog.MaxValue = _dataContext.ProposeBucketsCount; - _proposeBucketsProcessingProgressDialog.CurrentValue = _dataContext.ProposeBucketsProgress; - _proposeBucketsProcessingProgressDialog.CurrentPercentage = _dataContext.ProposeBucketsPercentage; - _proposeBucketsProcessingProgressDialog.Open(); - _dataContext.BucketProposalProgressChanged += OnBucketProposalProgressChanged; - - _dataContext.ProposeBuckets(); + _isProposeBucketsInfoDialogVisible = true; + StateHasChanged(); + await _dataContext.ProposeBuckets(); if (_dataContext.Transactions.Any(i => i.InModification)) _massEditEnabled = true; - - _proposeBucketsProcessingProgressDialog.Close(); - } - - private void OnBucketProposalProgressChanged(object sender, ProposeBucketChangedEventArgs e) - { - _proposeBucketsProcessingProgressDialog.UpdateProgress(e.NewValue, e.NewProgress); + _isProposeBucketsInfoDialogVisible = false; } - + async void SaveAllTransaction() { _massEditEnabled = false; diff --git a/OpenBudgeteer.Blazor/Shared/ProcessingProgressDialog.razor b/OpenBudgeteer.Blazor/Shared/ProcessingProgressDialog.razor deleted file mode 100644 index c88dbef..0000000 --- a/OpenBudgeteer.Blazor/Shared/ProcessingProgressDialog.razor +++ /dev/null @@ -1,52 +0,0 @@ - - -@if (_showBackdrop) -{ - -} - -@code { - public string Message { get; set; } - public int MinValue { get; set; } - public int MaxValue { get; set; } - public int CurrentValue { get; set; } - public int CurrentPercentage { get; set; } - - string _modalDisplay = "none;"; - string _modalClass = ""; - bool _showBackdrop = false; - - public void Open() - { - _modalDisplay = "block;"; - _modalClass = "Show"; - _showBackdrop = true; - StateHasChanged(); - } - - public void Close() - { - _modalDisplay = "none"; - _modalClass = ""; - _showBackdrop = false; - StateHasChanged(); - } - - public void UpdateProgress(int newValue, int newProgress) - { - CurrentValue = newValue; - CurrentPercentage = newProgress; - StateHasChanged(); - } -} diff --git a/OpenBudgeteer.Core/ViewModels/TransactionViewModel.cs b/OpenBudgeteer.Core/ViewModels/TransactionViewModel.cs index a102ce7..bfa77f8 100644 --- a/OpenBudgeteer.Core/ViewModels/TransactionViewModel.cs +++ b/OpenBudgeteer.Core/ViewModels/TransactionViewModel.cs @@ -355,22 +355,25 @@ public async Task CancelAllTransactionAsync() /// Starts process to propose the right for all Transactions /// /// Sets all Transactions into Modification Mode in case they have a "No Selection" Bucket - public void ProposeBuckets() + public async Task ProposeBuckets() { CurrentFilter = TransactionViewModelFilter.InModification; var unassignedTransactions = _transactions.Where(i => i.Buckets.First().SelectedBucket.BucketId == 0); ProposeBucketsCount = unassignedTransactions.Count(); ProposeBucketsProgress = 0; - foreach (var transaction in unassignedTransactions) - { - transaction.StartModification(); - transaction.ProposeBucket(); - ProposeBucketsProgress++; - ProposeBucketsPercentage = ProposeBucketsCount == 0 ? 0 : - Convert.ToInt32(Decimal.Divide(ProposeBucketsProgress, ProposeBucketsCount) * 100); - BucketProposalProgressChanged?.Invoke(this, - new ProposeBucketChangedEventArgs(ProposeBucketsProgress, ProposeBucketsPercentage)); - } + var proposalTasks = unassignedTransactions.Select(transaction => + Task.Run(() => + { + transaction.StartModification(); + transaction.ProposeBucket(); + ProposeBucketsProgress++; + ProposeBucketsPercentage = ProposeBucketsCount == 0 ? + 0 : Convert.ToInt32(Decimal.Divide(ProposeBucketsProgress, ProposeBucketsCount) * 100); + BucketProposalProgressChanged?.Invoke(this, + new ProposeBucketChangedEventArgs(ProposeBucketsProgress, ProposeBucketsPercentage)); + })); + + await Task.WhenAll(proposalTasks); } } From e5d95253d2e31270f77153d950c2a616fb98d0e6 Mon Sep 17 00:00:00 2001 From: Alexander Preibisch Date: Fri, 12 Nov 2021 22:19:08 +0100 Subject: [PATCH 40/50] Create docker-image-pre-release.yml --- .../workflows/docker-image-pre-release.yml | 24 +++++++++++++++++++ 1 file changed, 24 insertions(+) create mode 100644 .github/workflows/docker-image-pre-release.yml diff --git a/.github/workflows/docker-image-pre-release.yml b/.github/workflows/docker-image-pre-release.yml new file mode 100644 index 0000000..fa3403b --- /dev/null +++ b/.github/workflows/docker-image-pre-release.yml @@ -0,0 +1,24 @@ +name: Docker Image CI on pre-release + +on: + push: + branches: [ pre-release ] + +jobs: + build: + runs-on: ubuntu-latest + steps: + - name: Check out repo + uses: actions/checkout@v2 + + - name: Docker Login + uses: docker/login-action@v1.10.0 + with: + username: ${{ secrets.DOCKERHUB_USERNAME }} + password: ${{ secrets.DOCKERHUB_TOKEN }} + + - name: Build Docker image + run: docker build . -t "axelander/openbudgeteer:pre-release" -f "OpenBudgeteer.Blazor/Dockerfile" + + - name: Push Docker image + run: docker push axelander/openbudgeteer:pre-release From 30c58ae664e9c0da19ad808d88e1cd58be24e9e1 Mon Sep 17 00:00:00 2001 From: Alexander Preibisch Date: Fri, 12 Nov 2021 22:22:49 +0100 Subject: [PATCH 41/50] Create docker-image-master.yml --- .github/workflows/docker-image-master.yml | 24 +++++++++++++++++++++++ 1 file changed, 24 insertions(+) create mode 100644 .github/workflows/docker-image-master.yml diff --git a/.github/workflows/docker-image-master.yml b/.github/workflows/docker-image-master.yml new file mode 100644 index 0000000..b2e8cdd --- /dev/null +++ b/.github/workflows/docker-image-master.yml @@ -0,0 +1,24 @@ +name: Docker Image CI on master + +on: + push: + branches: [ master ] + +jobs: + build: + runs-on: ubuntu-latest + steps: + - name: Check out repo + uses: actions/checkout@v2 + + - name: Docker Login + uses: docker/login-action@v1.10.0 + with: + username: ${{ secrets.DOCKERHUB_USERNAME }} + password: ${{ secrets.DOCKERHUB_TOKEN }} + + - name: Build Docker image + run: docker build . -t "axelander/openbudgeteer:latest" -f "OpenBudgeteer.Blazor/Dockerfile" + + - name: Push Docker image + run: docker push axelander/openbudgeteer:latest From 37a372dee8ce173873c3cac36fdc83433bef1943 Mon Sep 17 00:00:00 2001 From: Alexander Preibisch Date: Fri, 12 Nov 2021 22:43:33 +0100 Subject: [PATCH 42/50] Update docker-image-pre-release.yml --- .../workflows/docker-image-pre-release.yml | 25 ++++++++++++++++++- 1 file changed, 24 insertions(+), 1 deletion(-) diff --git a/.github/workflows/docker-image-pre-release.yml b/.github/workflows/docker-image-pre-release.yml index fa3403b..c8fb0d7 100644 --- a/.github/workflows/docker-image-pre-release.yml +++ b/.github/workflows/docker-image-pre-release.yml @@ -3,9 +3,32 @@ name: Docker Image CI on pre-release on: push: branches: [ pre-release ] + +env: + DOTNET_VERSION: '6.0' # The .NET SDK version to use jobs: - build: + build-and-test: + runs-on: ubuntu-latest + steps: + - name: Check out repo + uses: actions/checkout@v2 + + - name: Setup dotnet + uses: actions/setup-dotnet@v1 + with: + dotnet-version: ${{ env.DOTNET_VERSION }} + + - name: Install Blazor dependencies + run: dotnet restore OpenBudgeteer.Blazor + + - name: Build Blazor + run: dotnet build OpenBudgeteer.Blazor --configuration Release --no-restore + + - name: Run Core Test Cases + run: dotnet test OpenBudgeteer.Core.Test + + deploy: runs-on: ubuntu-latest steps: - name: Check out repo From 3b008076042fe6720890eabb98392a9ad9ec16fe Mon Sep 17 00:00:00 2001 From: Alexander Preibisch Date: Fri, 12 Nov 2021 22:51:17 +0100 Subject: [PATCH 43/50] Create test-cases.yml --- .github/workflows/test-cases.yml | 30 ++++++++++++++++++++++++++++++ 1 file changed, 30 insertions(+) create mode 100644 .github/workflows/test-cases.yml diff --git a/.github/workflows/test-cases.yml b/.github/workflows/test-cases.yml new file mode 100644 index 0000000..f8e0fec --- /dev/null +++ b/.github/workflows/test-cases.yml @@ -0,0 +1,30 @@ +name: Test Cases + +on: + push: + branches: [ master, pre-release ] + +env: + DOTNET_VERSION: '6.0' # The .NET SDK version to use + +jobs: + build-and-test: + runs-on: ubuntu-latest + steps: + - name: Check out repo + uses: actions/checkout@v2 + + - name: Setup dotnet + uses: actions/setup-dotnet@v1 + with: + dotnet-version: ${{ env.DOTNET_VERSION }} + + - name: Install Blazor dependencies + run: dotnet restore OpenBudgeteer.Blazor + + - name: Build Blazor + run: dotnet build OpenBudgeteer.Blazor --configuration Release --no-restore + + - name: Run Core Test Cases + run: dotnet test OpenBudgeteer.Core.Test + From 9b44e55aa9a7c3a006a10f6736fa38d296b982f9 Mon Sep 17 00:00:00 2001 From: Alexander Preibisch Date: Fri, 12 Nov 2021 22:54:04 +0100 Subject: [PATCH 44/50] Update docker-image-pre-release.yml --- .../workflows/docker-image-pre-release.yml | 22 +------------------ 1 file changed, 1 insertion(+), 21 deletions(-) diff --git a/.github/workflows/docker-image-pre-release.yml b/.github/workflows/docker-image-pre-release.yml index c8fb0d7..01409be 100644 --- a/.github/workflows/docker-image-pre-release.yml +++ b/.github/workflows/docker-image-pre-release.yml @@ -1,4 +1,4 @@ -name: Docker Image CI on pre-release +name: Docker Image (pre-release) on: push: @@ -8,26 +8,6 @@ env: DOTNET_VERSION: '6.0' # The .NET SDK version to use jobs: - build-and-test: - runs-on: ubuntu-latest - steps: - - name: Check out repo - uses: actions/checkout@v2 - - - name: Setup dotnet - uses: actions/setup-dotnet@v1 - with: - dotnet-version: ${{ env.DOTNET_VERSION }} - - - name: Install Blazor dependencies - run: dotnet restore OpenBudgeteer.Blazor - - - name: Build Blazor - run: dotnet build OpenBudgeteer.Blazor --configuration Release --no-restore - - - name: Run Core Test Cases - run: dotnet test OpenBudgeteer.Core.Test - deploy: runs-on: ubuntu-latest steps: From 1a7ef631c2d1cbc55f74ca8dd037e38f4d3a93ed Mon Sep 17 00:00:00 2001 From: Alexander Preibisch Date: Fri, 12 Nov 2021 22:54:28 +0100 Subject: [PATCH 45/50] Update docker-image-master.yml --- .github/workflows/docker-image-master.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/docker-image-master.yml b/.github/workflows/docker-image-master.yml index b2e8cdd..803f8e5 100644 --- a/.github/workflows/docker-image-master.yml +++ b/.github/workflows/docker-image-master.yml @@ -1,4 +1,4 @@ -name: Docker Image CI on master +name: Docker Image (latest) on: push: From fa4fef57692616a566ff7655be44b9f397138fe0 Mon Sep 17 00:00:00 2001 From: Alexander Preibisch Date: Fri, 12 Nov 2021 23:07:00 +0100 Subject: [PATCH 46/50] Skip Test Case DistributeBudget_CheckDistributedMoney --- .../ViewModelTest/BucketViewModelTest.cs | 22 ++++++++++++++++--- 1 file changed, 19 insertions(+), 3 deletions(-) diff --git a/OpenBudgeteer.Core.Test/ViewModelTest/BucketViewModelTest.cs b/OpenBudgeteer.Core.Test/ViewModelTest/BucketViewModelTest.cs index b90951b..003752c 100644 --- a/OpenBudgeteer.Core.Test/ViewModelTest/BucketViewModelTest.cs +++ b/OpenBudgeteer.Core.Test/ViewModelTest/BucketViewModelTest.cs @@ -902,9 +902,25 @@ private async Task ExecuteBucketCreationAndTransactionMovem } } - public async Task DistributeBudget_CheckDistributedMoney( - IEnumerable> testBuckets - ) + public static IEnumerable TestData_DistributeBudget_CheckDistributedMoney + { + get + { + return new[] + { + new object[] + { + + }, + }; + } + } + + //TODO Finalize Test Case DistributeBudget_CheckDistributedMoney + [Theory (Skip = "Work in progress")] + [MemberData(nameof(TestData_DistributeBudget_CheckDistributedMoney))] + public void DistributeBudget_CheckDistributedMoney( + IEnumerable> testBuckets) { using (var dbContext = new DatabaseContext(_dbOptions)) { From 3098321903d41c83129527647b38fd22a927c7e1 Mon Sep 17 00:00:00 2001 From: Alexander Preibisch Date: Fri, 12 Nov 2021 23:31:45 +0100 Subject: [PATCH 47/50] Update README.md --- README.md | 24 +++++++++++++++++++++--- 1 file changed, 21 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index 3f7fce3..a8c999f 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,24 @@ -# OpenBudgeteer - -OpenBudgeteer is a budgeting app based on the Bucket Budgeting Principle and inspired by [YNAB](https://www.youneedabudget.com) and [Buckets](https://www.budgetwithbuckets.com). The Core is based on .NET Core and the MVVM Pattern, the Front End uses Blazor Server. +

+ OpenBudgeteer banner +

+ +

+ + Test Cases + + + Test Cases + + + Test Cases + +

+

+ Docker Pulls + GitHub release (latest by date) +

+ +OpenBudgeteer is a budgeting app based on the Bucket Budgeting Principle and inspired by [YNAB](https://www.youneedabudget.com) and [Buckets](https://www.budgetwithbuckets.com). The Core is based on .NET and the MVVM Pattern, the Front End uses Blazor Server. ![Screenshot 1](assets/screenshot1.png) From 9afd2f3bff488cc7b63a95f6bf8a52e10c5bc63c Mon Sep 17 00:00:00 2001 From: Alexander Preibisch Date: Fri, 12 Nov 2021 23:33:19 +0100 Subject: [PATCH 48/50] Update README.md --- README.md | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index a8c999f..8510975 100644 --- a/README.md +++ b/README.md @@ -1,8 +1,8 @@ -

+

OpenBudgeteer banner

-

+

Test Cases @@ -13,7 +13,7 @@ Test Cases

-

+

Docker Pulls GitHub release (latest by date)

From 9bcd71456dc4f127340fb2ca8cd452b782ab4395 Mon Sep 17 00:00:00 2001 From: Alexander Preibisch Date: Fri, 12 Nov 2021 23:36:09 +0100 Subject: [PATCH 49/50] Update README.md --- README.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 8510975..604ab47 100644 --- a/README.md +++ b/README.md @@ -7,10 +7,10 @@ Test Cases - Test Cases + Docker Image pre-release - Test Cases + Docker Image latest

From 4237350ff003b4e4dbe8e46ad40d1ba125fb5fa4 Mon Sep 17 00:00:00 2001 From: Alexander Preibisch Date: Sun, 14 Nov 2021 10:06:58 +0100 Subject: [PATCH 50/50] Update CHANGELOG.md --- CHANGELOG.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 7b1574c..e82dbc8 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,4 +1,4 @@ -### 1.4 (20xx-xx-xx) +### 1.4 (2021-11-14) * [Add] Info Dialog during Bucket proposal and optimized proposal performance #21 * [Add] Filter on Transaction Page #25 @@ -10,7 +10,7 @@ * [Changed] Updated dependencies. Thanks @kekkon * [Changed] Simplified dependency tree. Thanks @kekkon * [Changed] Progress calculation for several scenarios #26 #28 -* [Changed] Links and text due to swtich to Github #46 #47 +* [Changed] Links and text due to switch to Github #46 #47 * [Fixed] Reworked UI update handling to fix issues on refreshing data #22 * [Fixed] Missing Target Account update for newly created or updated Import Profiles #23 * [Fixed] `MonthOutputConverter.Convert` not using Culture. Thanks @kekkon