diff --git a/.gitignore b/.gitignore index 99229ad..8b3aafa 100644 --- a/.gitignore +++ b/.gitignore @@ -5,6 +5,8 @@ ## Custom files *.db +*.db-shm +*.db-wal *.DS_Store # User-specific files diff --git a/CHANGELOG.md b/CHANGELOG.md index 49e75aa..46ebcf7 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,9 @@ +### 1.4.1 (2021-11-28) + +* [Changed] Handling of Bucket Group creation (fixes also crashes during creation cancellation [#56](https://github.com/TheAxelander/OpenBudgeteer/issues/56)) +* [Fixed] Unable to add multiple Buckets during Bank Transaction creation [#55](https://github.com/TheAxelander/OpenBudgeteer/issues/55) +* [Fixed] Crash on Report Page using sqlite [#57](https://github.com/TheAxelander/OpenBudgeteer/issues/57) + ### 1.4 (2021-11-14) * [Add] Info Dialog during Bucket proposal and optimized proposal performance [#21](https://github.com/TheAxelander/OpenBudgeteer/issues/21) diff --git a/OpenBudgeteer.Blazor/Pages/Bucket.razor b/OpenBudgeteer.Blazor/Pages/Bucket.razor index b47323a..9c9060d 100644 --- a/OpenBudgeteer.Blazor/Pages/Bucket.razor +++ b/OpenBudgeteer.Blazor/Pages/Bucket.razor @@ -6,6 +6,7 @@ @using System.Drawing @using System.Globalization @using OpenBudgeteer.Core.Common +@using OpenBudgeteer.Core.Models @inject DbContextOptions<DatabaseContext> DbContextOptions @inject YearMonthSelectorViewModel YearMonthDataContext @@ -159,7 +160,7 @@ <div class="row"> <div class="col"> - <button class="btn btn-sm btn-primary header-action-button" @onclick="@CreateGroup">Create Bucket Group</button> + <button class="btn btn-sm btn-primary header-action-button" @onclick="@ShowNewBucketGroupDialog">Create Bucket Group</button> <button class="btn btn-sm btn-primary header-action-button" @onclick="@DistributeBudget">Distribute Budget</button> <button class="btn btn-sm btn-primary header-action-button" @onclick="@(() => _dataContext.ChangeBucketGroupCollapse())">Collapse All</button> <button class="btn btn-sm btn-primary header-action-button" @onclick="@(() => _dataContext.ChangeBucketGroupCollapse(false))">Expend All</button> @@ -298,6 +299,13 @@ </table> } +<NewBucketGroupDialog + DataContext="@_newBucketGroupDialogDataContext" + IsDialogVisible="@_isNewBucketGroupModalDialogVisible" + OnCancelClickCallback="@(CancelNewBucketGroupDialog)" + OnSaveClickCallback="@(SaveAndCloseNewBucketGroupDialog)" + /> + <EditBucketDialog Title="Edit Bucket" DataContext="@_editBucketDialogDataContext" @@ -322,6 +330,9 @@ @code { BucketViewModel _dataContext; + BucketGroup _newBucketGroupDialogDataContext; + bool _isNewBucketGroupModalDialogVisible; + BucketViewModelItem _editBucketDialogDataContext; bool _isEditBucketModalDialogVisible; @@ -362,6 +373,30 @@ ShowEditBucketDialog(newBucket); } + void ShowNewBucketGroupDialog() + { + _newBucketGroupDialogDataContext = new BucketGroup + { + BucketGroupId = 0, + Name = string.Empty + }; + _isNewBucketGroupModalDialogVisible = true; + } + + async void SaveAndCloseNewBucketGroupDialog() + { + _isNewBucketGroupModalDialogVisible = false; + // Requested Position is last, so set right position number + if (_newBucketGroupDialogDataContext.Position == -1) + _newBucketGroupDialogDataContext.Position = _dataContext.BucketGroups.Count + 1; + await HandleResult(_dataContext.CreateGroup(_newBucketGroupDialogDataContext)); + } + + void CancelNewBucketGroupDialog() + { + _isNewBucketGroupModalDialogVisible = false; + } + void ShowEditBucketDialog(BucketViewModelItem bucket) { _editBucketDialogDataContext = bucket; diff --git a/OpenBudgeteer.Blazor/Shared/NavMenu.razor b/OpenBudgeteer.Blazor/Shared/NavMenu.razor index cfb8f57..cc5be44 100644 --- a/OpenBudgeteer.Blazor/Shared/NavMenu.razor +++ b/OpenBudgeteer.Blazor/Shared/NavMenu.razor @@ -54,7 +54,7 @@ </div> <div class="navbar-text"> <span> - Version: 1.4 (<a href="https://github.com/TheAxelander/OpenBudgeteer/blob/master/CHANGELOG.md" target="_blank">Change Log</a>) + Version: 1.4.1 (<a href="https://github.com/TheAxelander/OpenBudgeteer/blob/master/CHANGELOG.md" target="_blank">Change Log</a>) </span> </div> </div> diff --git a/OpenBudgeteer.Blazor/Shared/NewBucketGroupDialog.razor b/OpenBudgeteer.Blazor/Shared/NewBucketGroupDialog.razor new file mode 100644 index 0000000..c08f82a --- /dev/null +++ b/OpenBudgeteer.Blazor/Shared/NewBucketGroupDialog.razor @@ -0,0 +1,73 @@ +@using OpenBudgeteer.Core.ViewModels.ItemViewModels +@using System.Drawing +@using OpenBudgeteer.Core.Models +@if (IsDialogVisible) + { + <div class="modal fade show" style=" display: block;"> + <div class="modal-dialog modal-dialog-scrollable" style="max-width: 600px"> + <div class="modal-content"> + <div class="modal-header"> + <h4 class="modal-title">New Bucket Group</h4> + <button type="button" class="close" data-dismiss="modal" @onclick="OnCancelClickCallback">×</button> + </div> + <div class="modal-body"> + <div class="form-group"> + <label>Name:</label> + <input class="form-control form-control-sm" type="text" @bind="DataContext.Name"/> + </div> + <div class="form-group"> + <label>Position:</label> + <div> + <div class="form-check form-check-inline"> + <input + class="form-check-input" + type="radio" + name="newBucketGroupDialogPositionOptions" + id="newBucketGroupDialogPositionOption1" + value="firstPosition" + checked + @onchange="@PositionSelectionChanged"> + <label class="form-check-label" for="newBucketGroupDialogPositionOption1">Set on first position</label> + </div> + <div class="form-check form-check-inline"> + <input + class="form-check-input" + type="radio" + name="newBucketGroupDialogPositionOptions" + id="newBucketGroupDialogPositionOption2" + value="lastPosition" + @onchange="@PositionSelectionChanged"> + <label class="form-check-label" for="newBucketGroupDialogPositionOption2">Set on last position</label> + </div> + </div> + </div> + </div> + <div class="modal-footer"> + <button type="button" class="btn btn-primary" data-dismiss="modal" @onclick="OnSaveClickCallback">Save</button> + <button type="button" class="btn btn-danger" data-dismiss="modal" @onclick="OnCancelClickCallback">Cancel</button> + </div> + </div> + </div> + </div> + <div class="modal-backdrop fade show"></div> + } + +@code { + [Parameter] + public BucketGroup DataContext { get; set; } + + [Parameter] + public bool IsDialogVisible { get; set; } + + [Parameter] + public EventCallback<MouseEventArgs> OnCancelClickCallback { get; set; } + + [Parameter] + public EventCallback<MouseEventArgs> OnSaveClickCallback { get; set; } + + private void PositionSelectionChanged(ChangeEventArgs eventArgs) + { + var selectedOption = eventArgs.Value as string; + DataContext.Position = selectedOption == "firstPosition" ? 0 : -1; + } +} diff --git a/OpenBudgeteer.Core/ViewModels/BucketViewModel.cs b/OpenBudgeteer.Core/ViewModels/BucketViewModel.cs index ffe3a2d..26829ab 100644 --- a/OpenBudgeteer.Core/ViewModels/BucketViewModel.cs +++ b/OpenBudgeteer.Core/ViewModels/BucketViewModel.cs @@ -191,7 +191,8 @@ public ViewModelOperationResult CreateGroup() bucketGroup.BucketGroup.Position++; dbContext.UpdateBucketGroup(bucketGroup.BucketGroup); } - if (dbContext.CreateBucketGroup(newGroup) == 0) return new ViewModelOperationResult(false, "Unable to write changes to database"); + if (dbContext.CreateBucketGroup(newGroup) == 0) + return new ViewModelOperationResult(false, "Unable to write changes to database"); } var newBucketGroupViewModelItem = @@ -203,6 +204,41 @@ public ViewModelOperationResult CreateGroup() return new ViewModelOperationResult(true); } + /// <summary> + /// Creates a new <see cref="BucketGroup"/> and adds it to ViewModel and Database. + /// Will be added on the requested position. + /// </summary> + /// <remarks>Triggers <see cref="ViewModelReloadRequired"/></remarks> + /// <param name="newBucketGroup">Instance of <see cref="BucketGroup"/> which needs to be created in database</param> + /// <returns>Object which contains information and results of this method</returns> + public ViewModelOperationResult CreateGroup(BucketGroup newBucketGroup) + { + if (newBucketGroup is null) + return new ViewModelOperationResult(false, "Unable to create Bucket Group"); + if (newBucketGroup.Name == string.Empty) + return new ViewModelOperationResult(false, "Bucket Group Name cannot be empty"); + + // Set Id to 0 to enable creation + newBucketGroup.BucketGroupId = 0; + + // Save Position, append Bucket Group and later move it to requested Position + var requestedPosition = newBucketGroup.Position; + newBucketGroup.Position = BucketGroups.Count + 1; + + using (var dbContext = new DatabaseContext(_dbOptions)) + { + if (dbContext.CreateBucketGroup(newBucketGroup) == 0) + return new ViewModelOperationResult(false, "Unable to write changes to database"); + + var newlyCreatedBucketGroup = dbContext.BucketGroup.OrderBy(i => i.BucketGroupId).Last(); + var newBucketGroupViewModelItem = new BucketGroupViewModelItem(_dbOptions, newlyCreatedBucketGroup, + _yearMonthViewModel.CurrentMonth); + newBucketGroupViewModelItem.MoveGroup(requestedPosition - newBucketGroupViewModelItem.BucketGroup.Position); + } + + return new ViewModelOperationResult(true, true); + } + /// <summary> /// Starts deletion process in the passed <see cref="BucketGroupViewModelItem"/> and updates positions of /// all other <see cref="BucketGroup"/> accordingly diff --git a/OpenBudgeteer.Core/ViewModels/ItemViewModels/TransactionViewModelItem.cs b/OpenBudgeteer.Core/ViewModels/ItemViewModels/TransactionViewModelItem.cs index 041b118..b584fc2 100644 --- a/OpenBudgeteer.Core/ViewModels/ItemViewModels/TransactionViewModelItem.cs +++ b/OpenBudgeteer.Core/ViewModels/ItemViewModels/TransactionViewModelItem.cs @@ -91,7 +91,9 @@ public TransactionViewModelItem() /// </summary> /// <param name="dbOptions">Options to connect to a database</param> /// <param name="yearMonthViewModel">YearMonth ViewModel instance</param> - public TransactionViewModelItem(DbContextOptions<DatabaseContext> dbOptions, YearMonthSelectorViewModel yearMonthViewModel) : this() + /// <param name="withEmptyBucket">Optionally creates an empty Bucket Assignment</param> + public TransactionViewModelItem(DbContextOptions<DatabaseContext> dbOptions, YearMonthSelectorViewModel yearMonthViewModel, + bool withEmptyBucket = false) : this() { _dbOptions = dbOptions; _yearMonthViewModel = yearMonthViewModel; @@ -114,6 +116,14 @@ public TransactionViewModelItem(DbContextOptions<DatabaseContext> dbOptions, Yea } } SelectedAccount = AvailableAccounts.First(); + // Create an empty Bucket Assignment if requested (required for "Create new Transaction") + if (withEmptyBucket) + { + var emptyBucket = new PartialBucketViewModelItem(dbOptions, yearMonthViewModel.CurrentMonth); + emptyBucket.AmountChanged += CheckBucketAssignments; + emptyBucket.DeleteAssignmentRequest += DeleteRequestedBucketAssignment; + Buckets.Add(emptyBucket); + } } /// <summary> @@ -123,7 +133,8 @@ public TransactionViewModelItem(DbContextOptions<DatabaseContext> dbOptions, Yea /// <param name="yearMonthViewModel">YearMonth ViewModel instance</param> /// <param name="transaction">Transaction instance</param> /// <param name="withBuckets">Include assigned Buckets</param> - public TransactionViewModelItem(DbContextOptions<DatabaseContext> dbOptions, YearMonthSelectorViewModel yearMonthViewModel, BankTransaction transaction, bool withBuckets = true) : this(dbOptions, yearMonthViewModel) + public TransactionViewModelItem(DbContextOptions<DatabaseContext> dbOptions, YearMonthSelectorViewModel yearMonthViewModel, + BankTransaction transaction, bool withBuckets = true) : this(dbOptions, yearMonthViewModel) { if (withBuckets) { @@ -226,7 +237,8 @@ public TransactionViewModelItem(BucketMovement bucketMovement) : this() /// <param name="yearMonthViewModel">YearMonth ViewModel instance</param> /// <param name="transaction">Transaction instance</param> /// <returns>New ViewModel instance</returns> - public static async Task<TransactionViewModelItem> CreateAsync(DbContextOptions<DatabaseContext> dbOptions, YearMonthSelectorViewModel yearMonthViewModel, BankTransaction transaction) + public static async Task<TransactionViewModelItem> CreateAsync(DbContextOptions<DatabaseContext> dbOptions, + YearMonthSelectorViewModel yearMonthViewModel, BankTransaction transaction) { return await Task.Run(() => new TransactionViewModelItem(dbOptions, yearMonthViewModel, transaction)); } @@ -239,7 +251,8 @@ public static async Task<TransactionViewModelItem> CreateAsync(DbContextOptions< /// <param name="yearMonthViewModel">YearMonth ViewModel instance</param> /// <param name="transaction">Transaction instance</param> /// <returns>New ViewModel instance</returns> - public static async Task<TransactionViewModelItem> CreateWithoutBucketsAsync(DbContextOptions<DatabaseContext> dbOptions, YearMonthSelectorViewModel yearMonthViewModel, BankTransaction transaction) + public static async Task<TransactionViewModelItem> CreateWithoutBucketsAsync(DbContextOptions<DatabaseContext> dbOptions, + YearMonthSelectorViewModel yearMonthViewModel, BankTransaction transaction) { return await Task.Run(() => new TransactionViewModelItem(dbOptions, yearMonthViewModel, transaction, false)); } diff --git a/OpenBudgeteer.Core/ViewModels/ReportViewModel.cs b/OpenBudgeteer.Core/ViewModels/ReportViewModel.cs index 94cc8b9..36dec23 100644 --- a/OpenBudgeteer.Core/ViewModels/ReportViewModel.cs +++ b/OpenBudgeteer.Core/ViewModels/ReportViewModel.cs @@ -198,10 +198,12 @@ public async Task<List<Tuple<DateTime, decimal>>> LoadBankBalancesAsync(int mont for (int monthIndex = months; monthIndex >= 0; monthIndex--) { var month = currentMonth.AddMonths(monthIndex * -1); - var bankBalance = dbContext.BankTransaction + var bankTransactions = dbContext.BankTransaction .Where(i => i.TransactionDate < month.AddMonths(1)) .OrderBy(i => i.TransactionDate) - .Sum(i => i.Amount); + .ToList(); + // Query split required due to incompatibility of decimal Sum operation on sqlite (see issue 57) + var bankBalance = bankTransactions.Sum(i => i.Amount); result.Add(new Tuple<DateTime, decimal>(month, bankBalance)); } } @@ -243,7 +245,7 @@ public async Task<List<MonthlyBucketExpensesReportViewModelItem>> LoadMonthExpen if (latestVersion.BucketType != 2) continue; using (var budgetedTransactionDbContext = new DatabaseContext(_dbOptions)) { - var queryResults = budgetedTransactionDbContext.BankTransaction + var queryScope = budgetedTransactionDbContext.BankTransaction // Join with BudgetedTransaction .Join(budgetedTransactionDbContext.BudgetedTransaction, transaction => transaction.TransactionId, @@ -256,6 +258,9 @@ public async Task<List<MonthlyBucketExpensesReportViewModelItem>> LoadMonthExpen // 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)) + .ToList(); + // Query split required due to incompatibility of decimal Sum operation on sqlite (see issue 57) + var queryResults = queryScope // Group the results per YearMonth .GroupBy(i => new DateTime(i.Transaction.TransactionDate.Year, i.Transaction.TransactionDate.Month, 1)) // Create a new Grouped Object diff --git a/OpenBudgeteer.Core/ViewModels/TransactionViewModel.cs b/OpenBudgeteer.Core/ViewModels/TransactionViewModel.cs index bfa77f8..aa01e01 100644 --- a/OpenBudgeteer.Core/ViewModels/TransactionViewModel.cs +++ b/OpenBudgeteer.Core/ViewModels/TransactionViewModel.cs @@ -297,8 +297,7 @@ public ViewModelOperationResult CreateItem() /// </summary> public void ResetNewTransaction() { - NewTransaction = new TransactionViewModelItem(_dbOptions, _yearMonthViewModel); - NewTransaction.Buckets.Add(new PartialBucketViewModelItem(_dbOptions, _yearMonthViewModel.CurrentMonth)); + NewTransaction = new TransactionViewModelItem(_dbOptions, _yearMonthViewModel, true); } /// <summary> diff --git a/README.md b/README.md index 63e2746..604ab47 100644 --- a/README.md +++ b/README.md @@ -22,8 +22,6 @@ OpenBudgeteer is a budgeting app based on the Bucket Budgeting Principle and ins ![Screenshot 1](assets/screenshot1.png) -![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: