From b6b794f0b4898b96c6351274e7315eb8bb85ebdb Mon Sep 17 00:00:00 2001
From: Ross Miller
Date: Fri, 6 Mar 2026 11:05:01 +0000
Subject: [PATCH] Refactor. Add data layer
---
.../ApplicationDbContext.cs | 9 +-
BudgetApp.Data/BudgetApp.Data.csproj | 22 +
.../Entities}/Account.cs | 2 +-
.../Entities}/ApplicationUser.cs | 2 +-
.../Entities}/Category.cs | 2 +-
BudgetApp.Data/Entities/RecurrenceType.cs | 10 +
.../Entities}/Salary.cs | 2 +-
.../Entities}/Transaction.cs | 17 +-
BudgetApp.Data/Entities/TransactionType.cs | 7 +
.../Interfaces/IAccountRepository.cs | 14 +
.../Interfaces/ICategoryRepository.cs | 12 +
.../Interfaces/ISalaryRepository.cs | 12 +
.../Interfaces/ITransactionRepository.cs | 20 +
...AddCurrencyCodeAndCategoryIcon.Designer.cs | 4 +-
.../AddCurrencyCodeAndCategoryIcon.cs | 41 ++
.../AddIsDefaultToCategory.Designer.cs | 4 +-
.../Migrations/AddIsDefaultToCategory.cs | 28 ++
.../AddIsPaidToTransaction.Designer.cs | 4 +-
.../Migrations/AddIsPaidToTransaction.cs | 28 ++
.../AddRecurringTransactions.Designer.cs | 4 +-
.../Migrations/AddRecurringTransactions.cs | 69 +++
.../Migrations/AddSalary.Designer.cs | 4 +-
BudgetApp.Data/Migrations/AddSalary.cs | 102 ++++
.../ApplicationDbContextModelSnapshot.cs | 4 +-
.../Migrations/InitialCreate.Designer.cs | 4 +-
BudgetApp.Data/Migrations/InitialCreate.cs | 321 ++++++++++++
.../Repositories/AccountRepository.cs | 35 +-
.../Repositories/CategoryRepository.cs | 33 +-
.../Repositories/SalaryRepository.cs | 37 +-
.../Repositories/TransactionRepository.cs | 84 ++--
BudgetApp.Tests/BudgetApp.Tests.csproj | 1 +
.../Helpers/TestDbContextFactory.cs | 1 +
BudgetApp.Tests/Models/AccountModelTests.cs | 2 +-
.../Services/AccountServiceTests.cs | 26 +-
.../Services/CategoryServiceTests.cs | 32 +-
.../Services/DashboardServiceTests.cs | 22 +-
.../Services/TransactionServiceTests.cs | 59 +--
BudgetApp.slnx | 1 +
BudgetApp/BudgetApp.csproj | 10 +-
BudgetApp/Components/Layout/NavMenu.razor | 48 +-
BudgetApp/Components/Layout/NavMenu.razor.cs | 43 ++
BudgetApp/Components/Pages/About.razor | 36 +-
BudgetApp/Components/Pages/About.razor.cs | 38 ++
BudgetApp/Components/Pages/Accounts.razor | 162 +-----
BudgetApp/Components/Pages/Accounts.razor.cs | 136 +++++
BudgetApp/Components/Pages/Categories.razor | 133 +----
.../Components/Pages/Categories.razor.cs | 118 +++++
BudgetApp/Components/Pages/Home.razor | 471 +++---------------
BudgetApp/Components/Pages/Home.razor.cs | 371 ++++++++++++++
BudgetApp/Components/Pages/Login.razor | 80 +--
BudgetApp/Components/Pages/Login.razor.cs | 74 +++
BudgetApp/Components/Pages/Logout.razor | 1 +
BudgetApp/Components/Pages/Register.razor | 47 +-
BudgetApp/Components/Pages/Register.razor.cs | 44 ++
BudgetApp/Components/Pages/Reports.razor | 262 ++--------
BudgetApp/Components/Pages/Reports.razor.cs | 218 ++++++++
BudgetApp/Components/Pages/Salary.razor | 140 +-----
BudgetApp/Components/Pages/Salary.razor.cs | 113 +++++
BudgetApp/Components/Pages/Settings.razor | 198 +-------
BudgetApp/Components/Pages/Settings.razor.cs | 159 ++++++
BudgetApp/Components/Pages/Transactions.razor | 466 +++--------------
.../Components/Pages/Transactions.razor.cs | 352 +++++++++++++
BudgetApp/Components/Pages/Weather.razor | 64 ---
BudgetApp/Components/ToastContainer.razor | 42 +-
BudgetApp/Components/ToastContainer.razor.cs | 40 ++
BudgetApp/Components/ToastValidator.razor | 46 +-
BudgetApp/Components/ToastValidator.razor.cs | 48 ++
BudgetApp/Components/_Imports.razor | 2 +
.../20260301152724_InitialCreate.cs | 323 ------------
...20260301155815_AddRecurringTransactions.cs | 71 ---
...01160737_AddCurrencyCodeAndCategoryIcon.cs | 42 --
.../20260305164332_AddIsPaidToTransaction.cs | 29 --
.../Migrations/20260305175841_AddSalary.cs | 104 ----
.../20260305181148_AddIsDefaultToCategory.cs | 29 --
BudgetApp/Program.cs | 17 +-
BudgetApp/Services/DashboardService.cs | 52 +-
BudgetApp/Services/Models/ChartData.cs | 8 +
BudgetApp/Services/Models/ChartDataset.cs | 9 +
BudgetApp/Services/Models/DashboardData.cs | 14 +
BudgetApp/budgetapp.db | Bin 167936 -> 167936 bytes
BudgetApp/budgetapp.db-shm | Bin 32768 -> 32768 bytes
BudgetApp/budgetapp.db-wal | Bin 449112 -> 41232 bytes
82 files changed, 2930 insertions(+), 2811 deletions(-)
rename {BudgetApp/Data => BudgetApp.Data}/ApplicationDbContext.cs (92%)
create mode 100644 BudgetApp.Data/BudgetApp.Data.csproj
rename {BudgetApp/Data => BudgetApp.Data/Entities}/Account.cs (97%)
rename {BudgetApp/Data => BudgetApp.Data/Entities}/ApplicationUser.cs (71%)
rename {BudgetApp/Data => BudgetApp.Data/Entities}/Category.cs (93%)
create mode 100644 BudgetApp.Data/Entities/RecurrenceType.cs
rename {BudgetApp/Data => BudgetApp.Data/Entities}/Salary.cs (97%)
rename {BudgetApp/Data => BudgetApp.Data/Entities}/Transaction.cs (84%)
create mode 100644 BudgetApp.Data/Entities/TransactionType.cs
create mode 100644 BudgetApp.Data/Interfaces/IAccountRepository.cs
create mode 100644 BudgetApp.Data/Interfaces/ICategoryRepository.cs
create mode 100644 BudgetApp.Data/Interfaces/ISalaryRepository.cs
create mode 100644 BudgetApp.Data/Interfaces/ITransactionRepository.cs
rename BudgetApp/Migrations/20260301160737_AddCurrencyCodeAndCategoryIcon.Designer.cs => BudgetApp.Data/Migrations/AddCurrencyCodeAndCategoryIcon.Designer.cs (99%)
create mode 100644 BudgetApp.Data/Migrations/AddCurrencyCodeAndCategoryIcon.cs
rename BudgetApp/Migrations/20260305181148_AddIsDefaultToCategory.Designer.cs => BudgetApp.Data/Migrations/AddIsDefaultToCategory.Designer.cs (99%)
create mode 100644 BudgetApp.Data/Migrations/AddIsDefaultToCategory.cs
rename BudgetApp/Migrations/20260305164332_AddIsPaidToTransaction.Designer.cs => BudgetApp.Data/Migrations/AddIsPaidToTransaction.Designer.cs (99%)
create mode 100644 BudgetApp.Data/Migrations/AddIsPaidToTransaction.cs
rename BudgetApp/Migrations/20260301155815_AddRecurringTransactions.Designer.cs => BudgetApp.Data/Migrations/AddRecurringTransactions.Designer.cs (99%)
create mode 100644 BudgetApp.Data/Migrations/AddRecurringTransactions.cs
rename BudgetApp/Migrations/20260305175841_AddSalary.Designer.cs => BudgetApp.Data/Migrations/AddSalary.Designer.cs (99%)
create mode 100644 BudgetApp.Data/Migrations/AddSalary.cs
rename {BudgetApp => BudgetApp.Data}/Migrations/ApplicationDbContextModelSnapshot.cs (99%)
rename BudgetApp/Migrations/20260301152724_InitialCreate.Designer.cs => BudgetApp.Data/Migrations/InitialCreate.Designer.cs (99%)
create mode 100644 BudgetApp.Data/Migrations/InitialCreate.cs
rename BudgetApp/Services/AccountService.cs => BudgetApp.Data/Repositories/AccountRepository.cs (67%)
rename BudgetApp/Services/CategoryService.cs => BudgetApp.Data/Repositories/CategoryRepository.cs (79%)
rename BudgetApp/Services/SalaryService.cs => BudgetApp.Data/Repositories/SalaryRepository.cs (79%)
rename BudgetApp/Services/TransactionService.cs => BudgetApp.Data/Repositories/TransactionRepository.cs (78%)
create mode 100644 BudgetApp/Components/Layout/NavMenu.razor.cs
create mode 100644 BudgetApp/Components/Pages/About.razor.cs
create mode 100644 BudgetApp/Components/Pages/Categories.razor.cs
create mode 100644 BudgetApp/Components/Pages/Home.razor.cs
create mode 100644 BudgetApp/Components/Pages/Register.razor.cs
create mode 100644 BudgetApp/Components/Pages/Reports.razor.cs
create mode 100644 BudgetApp/Components/Pages/Salary.razor.cs
create mode 100644 BudgetApp/Components/Pages/Settings.razor.cs
delete mode 100644 BudgetApp/Components/Pages/Weather.razor
create mode 100644 BudgetApp/Components/ToastContainer.razor.cs
create mode 100644 BudgetApp/Components/ToastValidator.razor.cs
delete mode 100644 BudgetApp/Migrations/20260301152724_InitialCreate.cs
delete mode 100644 BudgetApp/Migrations/20260301155815_AddRecurringTransactions.cs
delete mode 100644 BudgetApp/Migrations/20260301160737_AddCurrencyCodeAndCategoryIcon.cs
delete mode 100644 BudgetApp/Migrations/20260305164332_AddIsPaidToTransaction.cs
delete mode 100644 BudgetApp/Migrations/20260305175841_AddSalary.cs
delete mode 100644 BudgetApp/Migrations/20260305181148_AddIsDefaultToCategory.cs
create mode 100644 BudgetApp/Services/Models/ChartData.cs
create mode 100644 BudgetApp/Services/Models/ChartDataset.cs
create mode 100644 BudgetApp/Services/Models/DashboardData.cs
diff --git a/BudgetApp/Data/ApplicationDbContext.cs b/BudgetApp.Data/ApplicationDbContext.cs
similarity index 92%
rename from BudgetApp/Data/ApplicationDbContext.cs
rename to BudgetApp.Data/ApplicationDbContext.cs
index 9edeb55..883a974 100644
--- a/BudgetApp/Data/ApplicationDbContext.cs
+++ b/BudgetApp.Data/ApplicationDbContext.cs
@@ -1,15 +1,12 @@
+using BudgetApp.Data.Entities;
using Microsoft.AspNetCore.Identity.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore;
namespace BudgetApp.Data;
-public class ApplicationDbContext : IdentityDbContext
+public class ApplicationDbContext(DbContextOptions options)
+ : IdentityDbContext(options)
{
- public ApplicationDbContext(DbContextOptions options)
- : base(options)
- {
- }
-
public DbSet Accounts => Set();
public DbSet Transactions => Set();
public DbSet Categories => Set();
diff --git a/BudgetApp.Data/BudgetApp.Data.csproj b/BudgetApp.Data/BudgetApp.Data.csproj
new file mode 100644
index 0000000..becae22
--- /dev/null
+++ b/BudgetApp.Data/BudgetApp.Data.csproj
@@ -0,0 +1,22 @@
+
+
+
+ net10.0
+ enable
+ enable
+
+
+
+
+
+
+
+
+
+ runtime; build; native; contentfiles; analyzers; buildtransitive
+ all
+
+
+
+
+
diff --git a/BudgetApp/Data/Account.cs b/BudgetApp.Data/Entities/Account.cs
similarity index 97%
rename from BudgetApp/Data/Account.cs
rename to BudgetApp.Data/Entities/Account.cs
index db0354a..44ac8a6 100644
--- a/BudgetApp/Data/Account.cs
+++ b/BudgetApp.Data/Entities/Account.cs
@@ -1,6 +1,6 @@
using System.ComponentModel.DataAnnotations;
-namespace BudgetApp.Data;
+namespace BudgetApp.Data.Entities;
public class Account
{
diff --git a/BudgetApp/Data/ApplicationUser.cs b/BudgetApp.Data/Entities/ApplicationUser.cs
similarity index 71%
rename from BudgetApp/Data/ApplicationUser.cs
rename to BudgetApp.Data/Entities/ApplicationUser.cs
index 3e0c153..a64a86f 100644
--- a/BudgetApp/Data/ApplicationUser.cs
+++ b/BudgetApp.Data/Entities/ApplicationUser.cs
@@ -1,6 +1,6 @@
using Microsoft.AspNetCore.Identity;
-namespace BudgetApp.Data;
+namespace BudgetApp.Data.Entities;
public class ApplicationUser : IdentityUser
{
diff --git a/BudgetApp/Data/Category.cs b/BudgetApp.Data/Entities/Category.cs
similarity index 93%
rename from BudgetApp/Data/Category.cs
rename to BudgetApp.Data/Entities/Category.cs
index 85037e6..935bc1d 100644
--- a/BudgetApp/Data/Category.cs
+++ b/BudgetApp.Data/Entities/Category.cs
@@ -1,6 +1,6 @@
using System.ComponentModel.DataAnnotations;
-namespace BudgetApp.Data;
+namespace BudgetApp.Data.Entities;
public class Category
{
diff --git a/BudgetApp.Data/Entities/RecurrenceType.cs b/BudgetApp.Data/Entities/RecurrenceType.cs
new file mode 100644
index 0000000..45a7990
--- /dev/null
+++ b/BudgetApp.Data/Entities/RecurrenceType.cs
@@ -0,0 +1,10 @@
+namespace BudgetApp.Data.Entities;
+
+public enum RecurrenceType
+{
+ None,
+ Daily,
+ Weekly,
+ Monthly,
+ Yearly
+}
\ No newline at end of file
diff --git a/BudgetApp/Data/Salary.cs b/BudgetApp.Data/Entities/Salary.cs
similarity index 97%
rename from BudgetApp/Data/Salary.cs
rename to BudgetApp.Data/Entities/Salary.cs
index c26fc6a..bb8fca9 100644
--- a/BudgetApp/Data/Salary.cs
+++ b/BudgetApp.Data/Entities/Salary.cs
@@ -1,6 +1,6 @@
using System.ComponentModel.DataAnnotations;
-namespace BudgetApp.Data;
+namespace BudgetApp.Data.Entities;
public class Salary
{
diff --git a/BudgetApp/Data/Transaction.cs b/BudgetApp.Data/Entities/Transaction.cs
similarity index 84%
rename from BudgetApp/Data/Transaction.cs
rename to BudgetApp.Data/Entities/Transaction.cs
index 8cfaf05..b4ff280 100644
--- a/BudgetApp/Data/Transaction.cs
+++ b/BudgetApp.Data/Entities/Transaction.cs
@@ -1,21 +1,6 @@
using System.ComponentModel.DataAnnotations;
-namespace BudgetApp.Data;
-
-public enum TransactionType
-{
- Income,
- Expense
-}
-
-public enum RecurrenceType
-{
- None,
- Daily,
- Weekly,
- Monthly,
- Yearly
-}
+namespace BudgetApp.Data.Entities;
public class Transaction
{
diff --git a/BudgetApp.Data/Entities/TransactionType.cs b/BudgetApp.Data/Entities/TransactionType.cs
new file mode 100644
index 0000000..147f97c
--- /dev/null
+++ b/BudgetApp.Data/Entities/TransactionType.cs
@@ -0,0 +1,7 @@
+namespace BudgetApp.Data.Entities;
+
+public enum TransactionType
+{
+ Income,
+ Expense
+}
\ No newline at end of file
diff --git a/BudgetApp.Data/Interfaces/IAccountRepository.cs b/BudgetApp.Data/Interfaces/IAccountRepository.cs
new file mode 100644
index 0000000..79fbad2
--- /dev/null
+++ b/BudgetApp.Data/Interfaces/IAccountRepository.cs
@@ -0,0 +1,14 @@
+using BudgetApp.Data.Entities;
+
+namespace BudgetApp.Data.Interfaces;
+
+public interface IAccountRepository
+{
+ Task> GetAccountsAsync(string userId);
+ Task GetAccountAsync(int id, string userId);
+ Task CreateAccountAsync(Account account);
+ Task UpdateAccountAsync(Account account);
+ Task UpdateBalanceAsync(int id, string userId, decimal newBalance);
+ Task UpdateAccountDetailsAsync(int id, string userId, string name, string color, string currencyCode);
+ Task DeleteAccountAsync(int id, string userId);
+}
diff --git a/BudgetApp.Data/Interfaces/ICategoryRepository.cs b/BudgetApp.Data/Interfaces/ICategoryRepository.cs
new file mode 100644
index 0000000..8a1fba0
--- /dev/null
+++ b/BudgetApp.Data/Interfaces/ICategoryRepository.cs
@@ -0,0 +1,12 @@
+using BudgetApp.Data.Entities;
+
+namespace BudgetApp.Data.Interfaces;
+
+public interface ICategoryRepository
+{
+ Task> GetCategoriesAsync(string userId);
+ Task EnsureDefaultCategoriesAsync(string userId);
+ Task CreateCategoryAsync(Category category);
+ Task UpdateCategoryAsync(int id, string userId, string name, string icon);
+ Task DeleteCategoryAsync(int id, string userId);
+}
diff --git a/BudgetApp.Data/Interfaces/ISalaryRepository.cs b/BudgetApp.Data/Interfaces/ISalaryRepository.cs
new file mode 100644
index 0000000..0fe2096
--- /dev/null
+++ b/BudgetApp.Data/Interfaces/ISalaryRepository.cs
@@ -0,0 +1,12 @@
+using BudgetApp.Data.Entities;
+
+namespace BudgetApp.Data.Interfaces;
+
+public interface ISalaryRepository
+{
+ Task> GetSalariesAsync(string userId);
+ Task CreateSalaryAsync(Salary salary, DateTime windowEnd);
+ Task DeleteSalaryAsync(int id, string userId);
+ Task MaterializeSalaryTransactionsAsync(string userId, DateTime windowEnd);
+ Task GetNextPaydateAsync(int salaryId);
+}
diff --git a/BudgetApp.Data/Interfaces/ITransactionRepository.cs b/BudgetApp.Data/Interfaces/ITransactionRepository.cs
new file mode 100644
index 0000000..3c129c2
--- /dev/null
+++ b/BudgetApp.Data/Interfaces/ITransactionRepository.cs
@@ -0,0 +1,20 @@
+using BudgetApp.Data.Entities;
+
+namespace BudgetApp.Data.Interfaces;
+
+public interface ITransactionRepository
+{
+ Task> GetTransactionsAsync(string userId);
+ Task> GetRecentTransactionsAsync(string userId, int count = 5);
+ Task> GetUpcomingTransactionsAsync(string userId, int count = 5);
+ Task> GetTransactionsInRangeAsync(string userId, DateTime from, DateTime to);
+ Task> GetTransactionsByAccountAsync(int accountId, string userId);
+ Task CreateTransactionAsync(Transaction transaction);
+ Task UpdateTransactionAsync(int id, string userId, string description, decimal amount, DateTime date,
+ TransactionType type, int accountId, int? categoryId,
+ RecurrenceType recurrenceType = RecurrenceType.None, DateTime? recurrenceEndDate = null);
+ Task DeleteTransactionAsync(int id, string userId);
+ Task DeleteTransactionSeriesAsync(int id, string userId);
+ Task MarkAsPaidAsync(int id, string userId, bool isPaid = true);
+ Task MaterializeRecurringTransactionsAsync(string userId, DateTime? windowEnd = null);
+}
diff --git a/BudgetApp/Migrations/20260301160737_AddCurrencyCodeAndCategoryIcon.Designer.cs b/BudgetApp.Data/Migrations/AddCurrencyCodeAndCategoryIcon.Designer.cs
similarity index 99%
rename from BudgetApp/Migrations/20260301160737_AddCurrencyCodeAndCategoryIcon.Designer.cs
rename to BudgetApp.Data/Migrations/AddCurrencyCodeAndCategoryIcon.Designer.cs
index 9cab8eb..e03aa83 100644
--- a/BudgetApp/Migrations/20260301160737_AddCurrencyCodeAndCategoryIcon.Designer.cs
+++ b/BudgetApp.Data/Migrations/AddCurrencyCodeAndCategoryIcon.Designer.cs
@@ -1,4 +1,4 @@
-//
+//
using System;
using BudgetApp.Data;
using Microsoft.EntityFrameworkCore;
@@ -8,7 +8,7 @@ using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
#nullable disable
-namespace BudgetApp.Migrations
+namespace BudgetApp.Data.Migrations
{
[DbContext(typeof(ApplicationDbContext))]
[Migration("20260301160737_AddCurrencyCodeAndCategoryIcon")]
diff --git a/BudgetApp.Data/Migrations/AddCurrencyCodeAndCategoryIcon.cs b/BudgetApp.Data/Migrations/AddCurrencyCodeAndCategoryIcon.cs
new file mode 100644
index 0000000..ddb6c24
--- /dev/null
+++ b/BudgetApp.Data/Migrations/AddCurrencyCodeAndCategoryIcon.cs
@@ -0,0 +1,41 @@
+using Microsoft.EntityFrameworkCore.Migrations;
+
+#nullable disable
+
+namespace BudgetApp.Data.Migrations;
+
+///
+public partial class AddCurrencyCodeAndCategoryIcon : Migration
+{
+ ///
+ protected override void Up(MigrationBuilder migrationBuilder)
+ {
+ migrationBuilder.AddColumn(
+ name: "Icon",
+ table: "Categories",
+ type: "TEXT",
+ maxLength: 10,
+ nullable: false,
+ defaultValue: "");
+
+ migrationBuilder.AddColumn(
+ name: "CurrencyCode",
+ table: "Accounts",
+ type: "TEXT",
+ maxLength: 3,
+ nullable: false,
+ defaultValue: "");
+ }
+
+ ///
+ protected override void Down(MigrationBuilder migrationBuilder)
+ {
+ migrationBuilder.DropColumn(
+ name: "Icon",
+ table: "Categories");
+
+ migrationBuilder.DropColumn(
+ name: "CurrencyCode",
+ table: "Accounts");
+ }
+}
\ No newline at end of file
diff --git a/BudgetApp/Migrations/20260305181148_AddIsDefaultToCategory.Designer.cs b/BudgetApp.Data/Migrations/AddIsDefaultToCategory.Designer.cs
similarity index 99%
rename from BudgetApp/Migrations/20260305181148_AddIsDefaultToCategory.Designer.cs
rename to BudgetApp.Data/Migrations/AddIsDefaultToCategory.Designer.cs
index a622392..838e735 100644
--- a/BudgetApp/Migrations/20260305181148_AddIsDefaultToCategory.Designer.cs
+++ b/BudgetApp.Data/Migrations/AddIsDefaultToCategory.Designer.cs
@@ -1,4 +1,4 @@
-//
+//
using System;
using BudgetApp.Data;
using Microsoft.EntityFrameworkCore;
@@ -8,7 +8,7 @@ using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
#nullable disable
-namespace BudgetApp.Migrations
+namespace BudgetApp.Data.Migrations
{
[DbContext(typeof(ApplicationDbContext))]
[Migration("20260305181148_AddIsDefaultToCategory")]
diff --git a/BudgetApp.Data/Migrations/AddIsDefaultToCategory.cs b/BudgetApp.Data/Migrations/AddIsDefaultToCategory.cs
new file mode 100644
index 0000000..e893ea9
--- /dev/null
+++ b/BudgetApp.Data/Migrations/AddIsDefaultToCategory.cs
@@ -0,0 +1,28 @@
+using Microsoft.EntityFrameworkCore.Migrations;
+
+#nullable disable
+
+namespace BudgetApp.Data.Migrations;
+
+///
+public partial class AddIsDefaultToCategory : Migration
+{
+ ///
+ protected override void Up(MigrationBuilder migrationBuilder)
+ {
+ migrationBuilder.AddColumn(
+ name: "IsDefault",
+ table: "Categories",
+ type: "INTEGER",
+ nullable: false,
+ defaultValue: false);
+ }
+
+ ///
+ protected override void Down(MigrationBuilder migrationBuilder)
+ {
+ migrationBuilder.DropColumn(
+ name: "IsDefault",
+ table: "Categories");
+ }
+}
\ No newline at end of file
diff --git a/BudgetApp/Migrations/20260305164332_AddIsPaidToTransaction.Designer.cs b/BudgetApp.Data/Migrations/AddIsPaidToTransaction.Designer.cs
similarity index 99%
rename from BudgetApp/Migrations/20260305164332_AddIsPaidToTransaction.Designer.cs
rename to BudgetApp.Data/Migrations/AddIsPaidToTransaction.Designer.cs
index 877119d..cf0442a 100644
--- a/BudgetApp/Migrations/20260305164332_AddIsPaidToTransaction.Designer.cs
+++ b/BudgetApp.Data/Migrations/AddIsPaidToTransaction.Designer.cs
@@ -1,4 +1,4 @@
-//
+//
using System;
using BudgetApp.Data;
using Microsoft.EntityFrameworkCore;
@@ -8,7 +8,7 @@ using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
#nullable disable
-namespace BudgetApp.Migrations
+namespace BudgetApp.Data.Migrations
{
[DbContext(typeof(ApplicationDbContext))]
[Migration("20260305164332_AddIsPaidToTransaction")]
diff --git a/BudgetApp.Data/Migrations/AddIsPaidToTransaction.cs b/BudgetApp.Data/Migrations/AddIsPaidToTransaction.cs
new file mode 100644
index 0000000..63eb825
--- /dev/null
+++ b/BudgetApp.Data/Migrations/AddIsPaidToTransaction.cs
@@ -0,0 +1,28 @@
+using Microsoft.EntityFrameworkCore.Migrations;
+
+#nullable disable
+
+namespace BudgetApp.Data.Migrations;
+
+///
+public partial class AddIsPaidToTransaction : Migration
+{
+ ///
+ protected override void Up(MigrationBuilder migrationBuilder)
+ {
+ migrationBuilder.AddColumn(
+ name: "IsPaid",
+ table: "Transactions",
+ type: "INTEGER",
+ nullable: false,
+ defaultValue: false);
+ }
+
+ ///
+ protected override void Down(MigrationBuilder migrationBuilder)
+ {
+ migrationBuilder.DropColumn(
+ name: "IsPaid",
+ table: "Transactions");
+ }
+}
\ No newline at end of file
diff --git a/BudgetApp/Migrations/20260301155815_AddRecurringTransactions.Designer.cs b/BudgetApp.Data/Migrations/AddRecurringTransactions.Designer.cs
similarity index 99%
rename from BudgetApp/Migrations/20260301155815_AddRecurringTransactions.Designer.cs
rename to BudgetApp.Data/Migrations/AddRecurringTransactions.Designer.cs
index 5e7016e..e0d2210 100644
--- a/BudgetApp/Migrations/20260301155815_AddRecurringTransactions.Designer.cs
+++ b/BudgetApp.Data/Migrations/AddRecurringTransactions.Designer.cs
@@ -1,4 +1,4 @@
-//
+//
using System;
using BudgetApp.Data;
using Microsoft.EntityFrameworkCore;
@@ -8,7 +8,7 @@ using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
#nullable disable
-namespace BudgetApp.Migrations
+namespace BudgetApp.Data.Migrations
{
[DbContext(typeof(ApplicationDbContext))]
[Migration("20260301155815_AddRecurringTransactions")]
diff --git a/BudgetApp.Data/Migrations/AddRecurringTransactions.cs b/BudgetApp.Data/Migrations/AddRecurringTransactions.cs
new file mode 100644
index 0000000..3d11cda
--- /dev/null
+++ b/BudgetApp.Data/Migrations/AddRecurringTransactions.cs
@@ -0,0 +1,69 @@
+using Microsoft.EntityFrameworkCore.Migrations;
+
+#nullable disable
+
+namespace BudgetApp.Data.Migrations;
+
+///
+public partial class AddRecurringTransactions : Migration
+{
+ ///
+ protected override void Up(MigrationBuilder migrationBuilder)
+ {
+ migrationBuilder.AddColumn(
+ name: "ParentTransactionId",
+ table: "Transactions",
+ type: "INTEGER",
+ nullable: true);
+
+ migrationBuilder.AddColumn(
+ name: "RecurrenceEndDate",
+ table: "Transactions",
+ type: "TEXT",
+ nullable: true);
+
+ migrationBuilder.AddColumn(
+ name: "RecurrenceType",
+ table: "Transactions",
+ type: "INTEGER",
+ nullable: false,
+ defaultValue: 0);
+
+ migrationBuilder.CreateIndex(
+ name: "IX_Transactions_ParentTransactionId",
+ table: "Transactions",
+ column: "ParentTransactionId");
+
+ migrationBuilder.AddForeignKey(
+ name: "FK_Transactions_Transactions_ParentTransactionId",
+ table: "Transactions",
+ column: "ParentTransactionId",
+ principalTable: "Transactions",
+ principalColumn: "Id",
+ onDelete: ReferentialAction.SetNull);
+ }
+
+ ///
+ protected override void Down(MigrationBuilder migrationBuilder)
+ {
+ migrationBuilder.DropForeignKey(
+ name: "FK_Transactions_Transactions_ParentTransactionId",
+ table: "Transactions");
+
+ migrationBuilder.DropIndex(
+ name: "IX_Transactions_ParentTransactionId",
+ table: "Transactions");
+
+ migrationBuilder.DropColumn(
+ name: "ParentTransactionId",
+ table: "Transactions");
+
+ migrationBuilder.DropColumn(
+ name: "RecurrenceEndDate",
+ table: "Transactions");
+
+ migrationBuilder.DropColumn(
+ name: "RecurrenceType",
+ table: "Transactions");
+ }
+}
\ No newline at end of file
diff --git a/BudgetApp/Migrations/20260305175841_AddSalary.Designer.cs b/BudgetApp.Data/Migrations/AddSalary.Designer.cs
similarity index 99%
rename from BudgetApp/Migrations/20260305175841_AddSalary.Designer.cs
rename to BudgetApp.Data/Migrations/AddSalary.Designer.cs
index 951ae4a..d868977 100644
--- a/BudgetApp/Migrations/20260305175841_AddSalary.Designer.cs
+++ b/BudgetApp.Data/Migrations/AddSalary.Designer.cs
@@ -1,4 +1,4 @@
-//
+//
using System;
using BudgetApp.Data;
using Microsoft.EntityFrameworkCore;
@@ -8,7 +8,7 @@ using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
#nullable disable
-namespace BudgetApp.Migrations
+namespace BudgetApp.Data.Migrations
{
[DbContext(typeof(ApplicationDbContext))]
[Migration("20260305175841_AddSalary")]
diff --git a/BudgetApp.Data/Migrations/AddSalary.cs b/BudgetApp.Data/Migrations/AddSalary.cs
new file mode 100644
index 0000000..3bfcb30
--- /dev/null
+++ b/BudgetApp.Data/Migrations/AddSalary.cs
@@ -0,0 +1,102 @@
+using Microsoft.EntityFrameworkCore.Migrations;
+
+#nullable disable
+
+namespace BudgetApp.Data.Migrations;
+
+///
+public partial class AddSalary : Migration
+{
+ ///
+ protected override void Up(MigrationBuilder migrationBuilder)
+ {
+ migrationBuilder.AddColumn(
+ name: "SalaryId",
+ table: "Transactions",
+ type: "INTEGER",
+ nullable: true);
+
+ migrationBuilder.CreateTable(
+ name: "Salaries",
+ columns: table => new
+ {
+ Id = table.Column(type: "INTEGER", nullable: false)
+ .Annotation("Sqlite:Autoincrement", true),
+ Name = table.Column(type: "TEXT", maxLength: 200, nullable: false),
+ Amount = table.Column(type: "decimal(18,2)", nullable: false),
+ RecurrenceType = table.Column(type: "INTEGER", nullable: false),
+ StartDate = table.Column(type: "TEXT", nullable: false),
+ AccountId = table.Column(type: "INTEGER", nullable: false),
+ CategoryId = table.Column(type: "INTEGER", nullable: true),
+ UserId = table.Column(type: "TEXT", nullable: false)
+ },
+ constraints: table =>
+ {
+ table.PrimaryKey("PK_Salaries", x => x.Id);
+ table.ForeignKey(
+ name: "FK_Salaries_Accounts_AccountId",
+ column: x => x.AccountId,
+ principalTable: "Accounts",
+ principalColumn: "Id",
+ onDelete: ReferentialAction.Cascade);
+ table.ForeignKey(
+ name: "FK_Salaries_AspNetUsers_UserId",
+ column: x => x.UserId,
+ principalTable: "AspNetUsers",
+ principalColumn: "Id",
+ onDelete: ReferentialAction.Cascade);
+ table.ForeignKey(
+ name: "FK_Salaries_Categories_CategoryId",
+ column: x => x.CategoryId,
+ principalTable: "Categories",
+ principalColumn: "Id");
+ });
+
+ migrationBuilder.CreateIndex(
+ name: "IX_Transactions_SalaryId",
+ table: "Transactions",
+ column: "SalaryId");
+
+ migrationBuilder.CreateIndex(
+ name: "IX_Salaries_AccountId",
+ table: "Salaries",
+ column: "AccountId");
+
+ migrationBuilder.CreateIndex(
+ name: "IX_Salaries_CategoryId",
+ table: "Salaries",
+ column: "CategoryId");
+
+ migrationBuilder.CreateIndex(
+ name: "IX_Salaries_UserId",
+ table: "Salaries",
+ column: "UserId");
+
+ migrationBuilder.AddForeignKey(
+ name: "FK_Transactions_Salaries_SalaryId",
+ table: "Transactions",
+ column: "SalaryId",
+ principalTable: "Salaries",
+ principalColumn: "Id",
+ onDelete: ReferentialAction.SetNull);
+ }
+
+ ///
+ protected override void Down(MigrationBuilder migrationBuilder)
+ {
+ migrationBuilder.DropForeignKey(
+ name: "FK_Transactions_Salaries_SalaryId",
+ table: "Transactions");
+
+ migrationBuilder.DropTable(
+ name: "Salaries");
+
+ migrationBuilder.DropIndex(
+ name: "IX_Transactions_SalaryId",
+ table: "Transactions");
+
+ migrationBuilder.DropColumn(
+ name: "SalaryId",
+ table: "Transactions");
+ }
+}
\ No newline at end of file
diff --git a/BudgetApp/Migrations/ApplicationDbContextModelSnapshot.cs b/BudgetApp.Data/Migrations/ApplicationDbContextModelSnapshot.cs
similarity index 99%
rename from BudgetApp/Migrations/ApplicationDbContextModelSnapshot.cs
rename to BudgetApp.Data/Migrations/ApplicationDbContextModelSnapshot.cs
index 92d0001..0163219 100644
--- a/BudgetApp/Migrations/ApplicationDbContextModelSnapshot.cs
+++ b/BudgetApp.Data/Migrations/ApplicationDbContextModelSnapshot.cs
@@ -1,4 +1,4 @@
-//
+//
using System;
using BudgetApp.Data;
using Microsoft.EntityFrameworkCore;
@@ -7,7 +7,7 @@ using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
#nullable disable
-namespace BudgetApp.Migrations
+namespace BudgetApp.Data.Migrations
{
[DbContext(typeof(ApplicationDbContext))]
partial class ApplicationDbContextModelSnapshot : ModelSnapshot
diff --git a/BudgetApp/Migrations/20260301152724_InitialCreate.Designer.cs b/BudgetApp.Data/Migrations/InitialCreate.Designer.cs
similarity index 99%
rename from BudgetApp/Migrations/20260301152724_InitialCreate.Designer.cs
rename to BudgetApp.Data/Migrations/InitialCreate.Designer.cs
index 833968f..de2bdc1 100644
--- a/BudgetApp/Migrations/20260301152724_InitialCreate.Designer.cs
+++ b/BudgetApp.Data/Migrations/InitialCreate.Designer.cs
@@ -1,4 +1,4 @@
-//
+//
using System;
using BudgetApp.Data;
using Microsoft.EntityFrameworkCore;
@@ -8,7 +8,7 @@ using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
#nullable disable
-namespace BudgetApp.Migrations
+namespace BudgetApp.Data.Migrations
{
[DbContext(typeof(ApplicationDbContext))]
[Migration("20260301152724_InitialCreate")]
diff --git a/BudgetApp.Data/Migrations/InitialCreate.cs b/BudgetApp.Data/Migrations/InitialCreate.cs
new file mode 100644
index 0000000..ee00287
--- /dev/null
+++ b/BudgetApp.Data/Migrations/InitialCreate.cs
@@ -0,0 +1,321 @@
+using Microsoft.EntityFrameworkCore.Migrations;
+
+#nullable disable
+
+namespace BudgetApp.Data.Migrations;
+
+///
+public partial class InitialCreate : Migration
+{
+ ///
+ protected override void Up(MigrationBuilder migrationBuilder)
+ {
+ migrationBuilder.CreateTable(
+ name: "AspNetRoles",
+ columns: table => new
+ {
+ Id = table.Column(type: "TEXT", nullable: false),
+ Name = table.Column(type: "TEXT", maxLength: 256, nullable: true),
+ NormalizedName = table.Column(type: "TEXT", maxLength: 256, nullable: true),
+ ConcurrencyStamp = table.Column(type: "TEXT", nullable: true)
+ },
+ constraints: table =>
+ {
+ table.PrimaryKey("PK_AspNetRoles", x => x.Id);
+ });
+
+ migrationBuilder.CreateTable(
+ name: "AspNetUsers",
+ columns: table => new
+ {
+ Id = table.Column(type: "TEXT", nullable: false),
+ UserName = table.Column(type: "TEXT", maxLength: 256, nullable: true),
+ NormalizedUserName = table.Column(type: "TEXT", maxLength: 256, nullable: true),
+ Email = table.Column(type: "TEXT", maxLength: 256, nullable: true),
+ NormalizedEmail = table.Column(type: "TEXT", maxLength: 256, nullable: true),
+ EmailConfirmed = table.Column(type: "INTEGER", nullable: false),
+ PasswordHash = table.Column(type: "TEXT", nullable: true),
+ SecurityStamp = table.Column(type: "TEXT", nullable: true),
+ ConcurrencyStamp = table.Column(type: "TEXT", nullable: true),
+ PhoneNumber = table.Column(type: "TEXT", nullable: true),
+ PhoneNumberConfirmed = table.Column(type: "INTEGER", nullable: false),
+ TwoFactorEnabled = table.Column(type: "INTEGER", nullable: false),
+ LockoutEnd = table.Column(type: "TEXT", nullable: true),
+ LockoutEnabled = table.Column(type: "INTEGER", nullable: false),
+ AccessFailedCount = table.Column(type: "INTEGER", nullable: false)
+ },
+ constraints: table =>
+ {
+ table.PrimaryKey("PK_AspNetUsers", x => x.Id);
+ });
+
+ migrationBuilder.CreateTable(
+ name: "AspNetRoleClaims",
+ columns: table => new
+ {
+ Id = table.Column(type: "INTEGER", nullable: false)
+ .Annotation("Sqlite:Autoincrement", true),
+ RoleId = table.Column(type: "TEXT", nullable: false),
+ ClaimType = table.Column(type: "TEXT", nullable: true),
+ ClaimValue = table.Column(type: "TEXT", nullable: true)
+ },
+ constraints: table =>
+ {
+ table.PrimaryKey("PK_AspNetRoleClaims", x => x.Id);
+ table.ForeignKey(
+ name: "FK_AspNetRoleClaims_AspNetRoles_RoleId",
+ column: x => x.RoleId,
+ principalTable: "AspNetRoles",
+ principalColumn: "Id",
+ onDelete: ReferentialAction.Cascade);
+ });
+
+ migrationBuilder.CreateTable(
+ name: "Accounts",
+ columns: table => new
+ {
+ Id = table.Column(type: "INTEGER", nullable: false)
+ .Annotation("Sqlite:Autoincrement", true),
+ Name = table.Column(type: "TEXT", maxLength: 100, nullable: false),
+ CurrentBalance = table.Column(type: "decimal(18,2)", nullable: false),
+ Color = table.Column(type: "TEXT", maxLength: 7, nullable: false),
+ UserId = table.Column(type: "TEXT", nullable: false)
+ },
+ constraints: table =>
+ {
+ table.PrimaryKey("PK_Accounts", x => x.Id);
+ table.ForeignKey(
+ name: "FK_Accounts_AspNetUsers_UserId",
+ column: x => x.UserId,
+ principalTable: "AspNetUsers",
+ principalColumn: "Id",
+ onDelete: ReferentialAction.Cascade);
+ });
+
+ migrationBuilder.CreateTable(
+ name: "AspNetUserClaims",
+ columns: table => new
+ {
+ Id = table.Column(type: "INTEGER", nullable: false)
+ .Annotation("Sqlite:Autoincrement", true),
+ UserId = table.Column(type: "TEXT", nullable: false),
+ ClaimType = table.Column(type: "TEXT", nullable: true),
+ ClaimValue = table.Column(type: "TEXT", nullable: true)
+ },
+ constraints: table =>
+ {
+ table.PrimaryKey("PK_AspNetUserClaims", x => x.Id);
+ table.ForeignKey(
+ name: "FK_AspNetUserClaims_AspNetUsers_UserId",
+ column: x => x.UserId,
+ principalTable: "AspNetUsers",
+ principalColumn: "Id",
+ onDelete: ReferentialAction.Cascade);
+ });
+
+ migrationBuilder.CreateTable(
+ name: "AspNetUserLogins",
+ columns: table => new
+ {
+ LoginProvider = table.Column(type: "TEXT", nullable: false),
+ ProviderKey = table.Column(type: "TEXT", nullable: false),
+ ProviderDisplayName = table.Column(type: "TEXT", nullable: true),
+ UserId = table.Column(type: "TEXT", nullable: false)
+ },
+ constraints: table =>
+ {
+ table.PrimaryKey("PK_AspNetUserLogins", x => new { x.LoginProvider, x.ProviderKey });
+ table.ForeignKey(
+ name: "FK_AspNetUserLogins_AspNetUsers_UserId",
+ column: x => x.UserId,
+ principalTable: "AspNetUsers",
+ principalColumn: "Id",
+ onDelete: ReferentialAction.Cascade);
+ });
+
+ migrationBuilder.CreateTable(
+ name: "AspNetUserRoles",
+ columns: table => new
+ {
+ UserId = table.Column(type: "TEXT", nullable: false),
+ RoleId = table.Column(type: "TEXT", nullable: false)
+ },
+ constraints: table =>
+ {
+ table.PrimaryKey("PK_AspNetUserRoles", x => new { x.UserId, x.RoleId });
+ table.ForeignKey(
+ name: "FK_AspNetUserRoles_AspNetRoles_RoleId",
+ column: x => x.RoleId,
+ principalTable: "AspNetRoles",
+ principalColumn: "Id",
+ onDelete: ReferentialAction.Cascade);
+ table.ForeignKey(
+ name: "FK_AspNetUserRoles_AspNetUsers_UserId",
+ column: x => x.UserId,
+ principalTable: "AspNetUsers",
+ principalColumn: "Id",
+ onDelete: ReferentialAction.Cascade);
+ });
+
+ migrationBuilder.CreateTable(
+ name: "AspNetUserTokens",
+ columns: table => new
+ {
+ UserId = table.Column(type: "TEXT", nullable: false),
+ LoginProvider = table.Column(type: "TEXT", nullable: false),
+ Name = table.Column(type: "TEXT", nullable: false),
+ Value = table.Column(type: "TEXT", nullable: true)
+ },
+ constraints: table =>
+ {
+ table.PrimaryKey("PK_AspNetUserTokens", x => new { x.UserId, x.LoginProvider, x.Name });
+ table.ForeignKey(
+ name: "FK_AspNetUserTokens_AspNetUsers_UserId",
+ column: x => x.UserId,
+ principalTable: "AspNetUsers",
+ principalColumn: "Id",
+ onDelete: ReferentialAction.Cascade);
+ });
+
+ migrationBuilder.CreateTable(
+ name: "Categories",
+ columns: table => new
+ {
+ Id = table.Column(type: "INTEGER", nullable: false)
+ .Annotation("Sqlite:Autoincrement", true),
+ Name = table.Column(type: "TEXT", maxLength: 100, nullable: false),
+ UserId = table.Column(type: "TEXT", nullable: false)
+ },
+ constraints: table =>
+ {
+ table.PrimaryKey("PK_Categories", x => x.Id);
+ table.ForeignKey(
+ name: "FK_Categories_AspNetUsers_UserId",
+ column: x => x.UserId,
+ principalTable: "AspNetUsers",
+ principalColumn: "Id",
+ onDelete: ReferentialAction.Cascade);
+ });
+
+ migrationBuilder.CreateTable(
+ name: "Transactions",
+ columns: table => new
+ {
+ Id = table.Column(type: "INTEGER", nullable: false)
+ .Annotation("Sqlite:Autoincrement", true),
+ Description = table.Column(type: "TEXT", maxLength: 200, nullable: false),
+ Amount = table.Column(type: "decimal(18,2)", nullable: false),
+ Date = table.Column(type: "TEXT", nullable: false),
+ Type = table.Column(type: "INTEGER", nullable: false),
+ AccountId = table.Column(type: "INTEGER", nullable: false),
+ CategoryId = table.Column(type: "INTEGER", nullable: true)
+ },
+ constraints: table =>
+ {
+ table.PrimaryKey("PK_Transactions", x => x.Id);
+ table.ForeignKey(
+ name: "FK_Transactions_Accounts_AccountId",
+ column: x => x.AccountId,
+ principalTable: "Accounts",
+ principalColumn: "Id",
+ onDelete: ReferentialAction.Cascade);
+ table.ForeignKey(
+ name: "FK_Transactions_Categories_CategoryId",
+ column: x => x.CategoryId,
+ principalTable: "Categories",
+ principalColumn: "Id",
+ onDelete: ReferentialAction.SetNull);
+ });
+
+ migrationBuilder.CreateIndex(
+ name: "IX_Accounts_UserId",
+ table: "Accounts",
+ column: "UserId");
+
+ migrationBuilder.CreateIndex(
+ name: "IX_AspNetRoleClaims_RoleId",
+ table: "AspNetRoleClaims",
+ column: "RoleId");
+
+ migrationBuilder.CreateIndex(
+ name: "RoleNameIndex",
+ table: "AspNetRoles",
+ column: "NormalizedName",
+ unique: true);
+
+ migrationBuilder.CreateIndex(
+ name: "IX_AspNetUserClaims_UserId",
+ table: "AspNetUserClaims",
+ column: "UserId");
+
+ migrationBuilder.CreateIndex(
+ name: "IX_AspNetUserLogins_UserId",
+ table: "AspNetUserLogins",
+ column: "UserId");
+
+ migrationBuilder.CreateIndex(
+ name: "IX_AspNetUserRoles_RoleId",
+ table: "AspNetUserRoles",
+ column: "RoleId");
+
+ migrationBuilder.CreateIndex(
+ name: "EmailIndex",
+ table: "AspNetUsers",
+ column: "NormalizedEmail");
+
+ migrationBuilder.CreateIndex(
+ name: "UserNameIndex",
+ table: "AspNetUsers",
+ column: "NormalizedUserName",
+ unique: true);
+
+ migrationBuilder.CreateIndex(
+ name: "IX_Categories_UserId",
+ table: "Categories",
+ column: "UserId");
+
+ migrationBuilder.CreateIndex(
+ name: "IX_Transactions_AccountId",
+ table: "Transactions",
+ column: "AccountId");
+
+ migrationBuilder.CreateIndex(
+ name: "IX_Transactions_CategoryId",
+ table: "Transactions",
+ column: "CategoryId");
+ }
+
+ ///
+ protected override void Down(MigrationBuilder migrationBuilder)
+ {
+ migrationBuilder.DropTable(
+ name: "AspNetRoleClaims");
+
+ migrationBuilder.DropTable(
+ name: "AspNetUserClaims");
+
+ migrationBuilder.DropTable(
+ name: "AspNetUserLogins");
+
+ migrationBuilder.DropTable(
+ name: "AspNetUserRoles");
+
+ migrationBuilder.DropTable(
+ name: "AspNetUserTokens");
+
+ migrationBuilder.DropTable(
+ name: "Transactions");
+
+ migrationBuilder.DropTable(
+ name: "AspNetRoles");
+
+ migrationBuilder.DropTable(
+ name: "Accounts");
+
+ migrationBuilder.DropTable(
+ name: "Categories");
+
+ migrationBuilder.DropTable(
+ name: "AspNetUsers");
+ }
+}
\ No newline at end of file
diff --git a/BudgetApp/Services/AccountService.cs b/BudgetApp.Data/Repositories/AccountRepository.cs
similarity index 67%
rename from BudgetApp/Services/AccountService.cs
rename to BudgetApp.Data/Repositories/AccountRepository.cs
index 45dd303..4be0fb9 100644
--- a/BudgetApp/Services/AccountService.cs
+++ b/BudgetApp.Data/Repositories/AccountRepository.cs
@@ -1,15 +1,14 @@
+using BudgetApp.Data.Entities;
+using BudgetApp.Data.Interfaces;
using Microsoft.EntityFrameworkCore;
-using BudgetApp.Data;
-namespace BudgetApp.Services;
+namespace BudgetApp.Data.Repositories;
-public class AccountService(ApplicationDbContext db)
+public class AccountRepository(ApplicationDbContext db) : IAccountRepository
{
- private readonly ApplicationDbContext _db = db;
-
public async Task> GetAccountsAsync(string userId)
{
- return await _db.Accounts
+ return await db.Accounts
.Where(a => a.UserId == userId)
.OrderBy(a => a.Name)
.ToListAsync();
@@ -17,54 +16,54 @@ public class AccountService(ApplicationDbContext db)
public async Task GetAccountAsync(int id, string userId)
{
- return await _db.Accounts
+ return await db.Accounts
.FirstOrDefaultAsync(a => a.Id == id && a.UserId == userId);
}
public async Task CreateAccountAsync(Account account)
{
- _db.Accounts.Add(account);
- await _db.SaveChangesAsync();
+ db.Accounts.Add(account);
+ await db.SaveChangesAsync();
}
public async Task UpdateAccountAsync(Account account)
{
- _db.Accounts.Update(account);
- await _db.SaveChangesAsync();
+ db.Accounts.Update(account);
+ await db.SaveChangesAsync();
}
public async Task UpdateBalanceAsync(int id, string userId, decimal newBalance)
{
- var account = await _db.Accounts
+ var account = await db.Accounts
.FirstOrDefaultAsync(a => a.Id == id && a.UserId == userId);
if (account is not null)
{
account.CurrentBalance = newBalance;
- await _db.SaveChangesAsync();
+ await db.SaveChangesAsync();
}
}
public async Task UpdateAccountDetailsAsync(int id, string userId, string name, string color, string currencyCode)
{
- var account = await _db.Accounts
+ var account = await db.Accounts
.FirstOrDefaultAsync(a => a.Id == id && a.UserId == userId);
if (account is not null)
{
account.Name = name;
account.Color = color;
account.CurrencyCode = currencyCode;
- await _db.SaveChangesAsync();
+ await db.SaveChangesAsync();
}
}
public async Task DeleteAccountAsync(int id, string userId)
{
- var account = await _db.Accounts
+ var account = await db.Accounts
.FirstOrDefaultAsync(a => a.Id == id && a.UserId == userId);
if (account is not null)
{
- _db.Accounts.Remove(account);
- await _db.SaveChangesAsync();
+ db.Accounts.Remove(account);
+ await db.SaveChangesAsync();
}
}
}
diff --git a/BudgetApp/Services/CategoryService.cs b/BudgetApp.Data/Repositories/CategoryRepository.cs
similarity index 79%
rename from BudgetApp/Services/CategoryService.cs
rename to BudgetApp.Data/Repositories/CategoryRepository.cs
index e7cc5af..9f04ef8 100644
--- a/BudgetApp/Services/CategoryService.cs
+++ b/BudgetApp.Data/Repositories/CategoryRepository.cs
@@ -1,12 +1,11 @@
+using BudgetApp.Data.Entities;
+using BudgetApp.Data.Interfaces;
using Microsoft.EntityFrameworkCore;
-using BudgetApp.Data;
-namespace BudgetApp.Services;
+namespace BudgetApp.Data.Repositories;
-public class CategoryService(ApplicationDbContext db)
+public class CategoryRepository(ApplicationDbContext db) : ICategoryRepository
{
- private readonly ApplicationDbContext _db = db;
-
private static readonly (string Name, string Icon)[] DefaultCategories =
[
("Groceries", "🛒"),
@@ -31,7 +30,7 @@ public class CategoryService(ApplicationDbContext db)
public async Task> GetCategoriesAsync(string userId)
{
- return await _db.Categories
+ return await db.Categories
.Where(c => c.UserId == userId)
.OrderBy(c => c.Name)
.ToListAsync();
@@ -39,7 +38,7 @@ public class CategoryService(ApplicationDbContext db)
public async Task EnsureDefaultCategoriesAsync(string userId)
{
- var existing = await _db.Categories
+ var existing = await db.Categories
.Where(c => c.UserId == userId)
.ToListAsync();
@@ -53,8 +52,8 @@ public class CategoryService(ApplicationDbContext db)
UserId = userId
});
- _db.Categories.AddRange(defaults);
- await _db.SaveChangesAsync();
+ db.Categories.AddRange(defaults);
+ await db.SaveChangesAsync();
return;
}
@@ -78,38 +77,38 @@ public class CategoryService(ApplicationDbContext db)
if (updated)
{
- await _db.SaveChangesAsync();
+ await db.SaveChangesAsync();
}
}
public async Task CreateCategoryAsync(Category category)
{
- _db.Categories.Add(category);
- await _db.SaveChangesAsync();
+ db.Categories.Add(category);
+ await db.SaveChangesAsync();
}
public async Task UpdateCategoryAsync(int id, string userId, string name, string icon)
{
- var category = await _db.Categories
+ var category = await db.Categories
.FirstOrDefaultAsync(c => c.Id == id && c.UserId == userId);
if (category is not null)
{
if (category.IsDefault) return;
category.Name = name;
category.Icon = icon;
- await _db.SaveChangesAsync();
+ await db.SaveChangesAsync();
}
}
public async Task DeleteCategoryAsync(int id, string userId)
{
- var category = await _db.Categories
+ var category = await db.Categories
.FirstOrDefaultAsync(c => c.Id == id && c.UserId == userId);
if (category is not null)
{
if (category.IsDefault) return;
- _db.Categories.Remove(category);
- await _db.SaveChangesAsync();
+ db.Categories.Remove(category);
+ await db.SaveChangesAsync();
}
}
}
diff --git a/BudgetApp/Services/SalaryService.cs b/BudgetApp.Data/Repositories/SalaryRepository.cs
similarity index 79%
rename from BudgetApp/Services/SalaryService.cs
rename to BudgetApp.Data/Repositories/SalaryRepository.cs
index 1e1bc37..f8d6825 100644
--- a/BudgetApp/Services/SalaryService.cs
+++ b/BudgetApp.Data/Repositories/SalaryRepository.cs
@@ -1,15 +1,14 @@
+using BudgetApp.Data.Entities;
+using BudgetApp.Data.Interfaces;
using Microsoft.EntityFrameworkCore;
-using BudgetApp.Data;
-namespace BudgetApp.Services;
+namespace BudgetApp.Data.Repositories;
-public class SalaryService(ApplicationDbContext db)
+public class SalaryRepository(ApplicationDbContext db) : ISalaryRepository
{
- private readonly ApplicationDbContext _db = db;
-
public async Task> GetSalariesAsync(string userId)
{
- return await _db.Salaries
+ return await db.Salaries
.Include(s => s.Account)
.Include(s => s.Category)
.Where(s => s.UserId == userId)
@@ -19,35 +18,35 @@ public class SalaryService(ApplicationDbContext db)
public async Task CreateSalaryAsync(Salary salary, DateTime windowEnd)
{
- var salaryCategoryId = await _db.Categories
+ var salaryCategoryId = await db.Categories
.Where(c => c.UserId == salary.UserId && c.Name == "Salary")
.Select(c => (int?)c.Id)
.FirstOrDefaultAsync();
salary.CategoryId = salaryCategoryId;
- _db.Salaries.Add(salary);
- await _db.SaveChangesAsync();
+ db.Salaries.Add(salary);
+ await db.SaveChangesAsync();
await MaterializeSalaryTransactionsAsync(salary.UserId, windowEnd);
}
public async Task DeleteSalaryAsync(int id, string userId)
{
- var exists = await _db.Salaries.AnyAsync(s => s.Id == id && s.UserId == userId);
+ var exists = await db.Salaries.AnyAsync(s => s.Id == id && s.UserId == userId);
if (!exists) return;
- await using var transaction = await _db.Database.BeginTransactionAsync();
+ await using var transaction = await db.Database.BeginTransactionAsync();
// Delete future unpaid transactions generated by this salary
- await _db.Transactions
+ await db.Transactions
.Where(t => t.SalaryId == id && t.Date.Date > DateTime.Today && !t.IsPaid)
.ExecuteDeleteAsync();
// Unlink past / paid transactions so they stay in history without the salary reference
- await _db.Transactions
+ await db.Transactions
.Where(t => t.SalaryId == id)
.ExecuteUpdateAsync(s => s.SetProperty(t => t.SalaryId, (int?)null));
- await _db.Salaries
+ await db.Salaries
.Where(s => s.Id == id && s.UserId == userId)
.ExecuteDeleteAsync();
@@ -56,7 +55,7 @@ public class SalaryService(ApplicationDbContext db)
public async Task MaterializeSalaryTransactionsAsync(string userId, DateTime windowEnd)
{
- var salaries = await _db.Salaries
+ var salaries = await db.Salaries
.Where(s => s.UserId == userId)
.ToListAsync();
@@ -64,7 +63,7 @@ public class SalaryService(ApplicationDbContext db)
foreach (var salary in salaries)
{
- var latestDate = await _db.Transactions
+ var latestDate = await db.Transactions
.Where(t => t.SalaryId == salary.Id)
.OrderByDescending(t => t.Date)
.Select(t => (DateTime?)t.Date)
@@ -93,14 +92,14 @@ public class SalaryService(ApplicationDbContext db)
if (newTransactions.Count > 0)
{
- _db.Transactions.AddRange(newTransactions);
- await _db.SaveChangesAsync();
+ db.Transactions.AddRange(newTransactions);
+ await db.SaveChangesAsync();
}
}
public async Task GetNextPaydateAsync(int salaryId)
{
- return await _db.Transactions
+ return await db.Transactions
.Where(t => t.SalaryId == salaryId && t.Date.Date >= DateTime.Today)
.OrderBy(t => t.Date)
.Select(t => (DateTime?)t.Date)
diff --git a/BudgetApp/Services/TransactionService.cs b/BudgetApp.Data/Repositories/TransactionRepository.cs
similarity index 78%
rename from BudgetApp/Services/TransactionService.cs
rename to BudgetApp.Data/Repositories/TransactionRepository.cs
index 6bb0eb0..01677dc 100644
--- a/BudgetApp/Services/TransactionService.cs
+++ b/BudgetApp.Data/Repositories/TransactionRepository.cs
@@ -1,15 +1,14 @@
+using BudgetApp.Data.Entities;
+using BudgetApp.Data.Interfaces;
using Microsoft.EntityFrameworkCore;
-using BudgetApp.Data;
-namespace BudgetApp.Services;
+namespace BudgetApp.Data.Repositories;
-public class TransactionService(ApplicationDbContext db)
+public class TransactionRepository(ApplicationDbContext db) : ITransactionRepository
{
- private readonly ApplicationDbContext _db = db;
-
public async Task> GetTransactionsAsync(string userId)
{
- return await _db.Transactions
+ return await db.Transactions
.Include(t => t.Account)
.Include(t => t.Category)
.Include(t => t.ParentTransaction)
@@ -20,7 +19,7 @@ public class TransactionService(ApplicationDbContext db)
public async Task> GetRecentTransactionsAsync(string userId, int count = 5)
{
- return await _db.Transactions
+ return await db.Transactions
.Include(t => t.Account)
.Include(t => t.Category)
.Include(t => t.ParentTransaction)
@@ -32,7 +31,7 @@ public class TransactionService(ApplicationDbContext db)
public async Task> GetUpcomingTransactionsAsync(string userId, int count = 5)
{
- return await _db.Transactions
+ return await db.Transactions
.Include(t => t.Account)
.Include(t => t.Category)
.Include(t => t.ParentTransaction)
@@ -44,7 +43,7 @@ public class TransactionService(ApplicationDbContext db)
public async Task> GetTransactionsInRangeAsync(string userId, DateTime from, DateTime to)
{
- return await _db.Transactions
+ return await db.Transactions
.Include(t => t.Account)
.Include(t => t.Category)
.Include(t => t.ParentTransaction)
@@ -55,7 +54,7 @@ public class TransactionService(ApplicationDbContext db)
public async Task> GetTransactionsByAccountAsync(int accountId, string userId)
{
- return await _db.Transactions
+ return await db.Transactions
.Include(t => t.Category)
.Where(t => t.AccountId == accountId && t.Account.UserId == userId)
.OrderByDescending(t => t.Date)
@@ -74,8 +73,8 @@ public class TransactionService(ApplicationDbContext db)
}
}
- _db.Transactions.Add(transaction);
- await _db.SaveChangesAsync();
+ db.Transactions.Add(transaction);
+ await db.SaveChangesAsync();
// Immediately materialize child occurrences for recurring transactions
if (transaction.RecurrenceType != RecurrenceType.None)
@@ -102,15 +101,17 @@ public class TransactionService(ApplicationDbContext db)
if (newTransactions.Count > 0)
{
- _db.Transactions.AddRange(newTransactions);
- await _db.SaveChangesAsync();
+ db.Transactions.AddRange(newTransactions);
+ await db.SaveChangesAsync();
}
}
}
- public async Task UpdateTransactionAsync(int id, string userId, string description, decimal amount, DateTime date, TransactionType type, int accountId, int? categoryId, RecurrenceType recurrenceType = RecurrenceType.None, DateTime? recurrenceEndDate = null)
+ public async Task UpdateTransactionAsync(int id, string userId, string description, decimal amount,
+ DateTime date, TransactionType type, int accountId, int? categoryId,
+ RecurrenceType recurrenceType = RecurrenceType.None, DateTime? recurrenceEndDate = null)
{
- var transaction = await _db.Transactions
+ var transaction = await db.Transactions
.Include(t => t.Account)
.FirstOrDefaultAsync(t => t.Id == id && t.Account.UserId == userId);
@@ -128,11 +129,10 @@ public class TransactionService(ApplicationDbContext db)
if (isPartOfSeries)
{
- // Cascade edits to all future occurrences in the series
var parentId = transaction.ParentTransactionId ?? transaction.Id;
var cutoffDate = transaction.Date.Date;
- var futureSiblings = await _db.Transactions
+ var futureSiblings = await db.Transactions
.Where(t => (t.Id == parentId || t.ParentTransactionId == parentId)
&& t.Id != id
&& t.Date.Date >= cutoffDate)
@@ -149,92 +149,84 @@ public class TransactionService(ApplicationDbContext db)
}
else
{
- // Standalone transaction — preserve existing recurrence state (None)
transaction.RecurrenceType = recurrenceType;
transaction.RecurrenceEndDate = recurrenceEndDate;
}
- await _db.SaveChangesAsync();
+ await db.SaveChangesAsync();
}
public async Task DeleteTransactionAsync(int id, string userId)
{
- var transaction = await _db.Transactions
+ var transaction = await db.Transactions
.FirstOrDefaultAsync(t => t.Id == id && t.Account.UserId == userId);
if (transaction is not null)
{
- _db.Transactions.Remove(transaction);
- await _db.SaveChangesAsync();
+ db.Transactions.Remove(transaction);
+ await db.SaveChangesAsync();
}
}
public async Task DeleteTransactionSeriesAsync(int id, string userId)
{
- var transaction = await _db.Transactions
+ var transaction = await db.Transactions
.Include(t => t.Account)
.FirstOrDefaultAsync(t => t.Id == id && t.Account.UserId == userId);
if (transaction is null) return;
- // Resolve the parent id: if this is a child, use its ParentTransactionId; otherwise use its own Id
var parentId = transaction.ParentTransactionId ?? transaction.Id;
-
- // Use the clicked transaction's date as the cutoff — delete this one and everything after it
var cutoffDate = transaction.Date.Date;
- var allChildren = await _db.Transactions
+ var allChildren = await db.Transactions
.Where(t => t.ParentTransactionId == parentId)
.ToListAsync();
var toDelete = allChildren.Where(c => c.Date.Date >= cutoffDate).ToList();
var toKeep = allChildren.Where(c => c.Date.Date < cutoffDate).ToList();
- _db.Transactions.RemoveRange(toDelete);
+ db.Transactions.RemoveRange(toDelete);
- // Handle the parent
var parent = parentId == transaction.Id
? transaction
- : await _db.Transactions
+ : await db.Transactions
.FirstOrDefaultAsync(t => t.Id == parentId && t.Account.UserId == userId);
if (parent is not null)
{
if (parent.Date.Date >= cutoffDate)
{
- // Parent is also at/after the cutoff — detach earlier children so
- // the DB-level ON DELETE SET NULL doesn't clear their link, then delete
foreach (var child in toKeep)
{
child.ParentTransactionId = null;
}
- _db.Transactions.Remove(parent);
+ db.Transactions.Remove(parent);
}
else
{
- // Parent is before the cutoff — cap recurrence so materialization stops
parent.RecurrenceEndDate = cutoffDate.AddDays(-1);
}
}
- await _db.SaveChangesAsync();
+ await db.SaveChangesAsync();
}
public async Task MarkAsPaidAsync(int id, string userId, bool isPaid = true)
{
- var transaction = await _db.Transactions
+ var transaction = await db.Transactions
.Include(t => t.Account)
.FirstOrDefaultAsync(t => t.Id == id && t.Account.UserId == userId);
if (transaction is null) return;
transaction.IsPaid = isPaid;
- await _db.SaveChangesAsync();
+ await db.SaveChangesAsync();
}
public async Task MaterializeRecurringTransactionsAsync(string userId, DateTime? windowEnd = null)
{
- var recurringParents = await _db.Transactions
+ var recurringParents = await db.Transactions
.Include(t => t.Account)
.Where(t => t.Account.UserId == userId
&& t.RecurrenceType != RecurrenceType.None
@@ -251,7 +243,7 @@ public class TransactionService(ApplicationDbContext db)
? parent.RecurrenceEndDate.Value
: horizon;
- var latestChildDate = await _db.Transactions
+ var latestChildDate = await db.Transactions
.Where(t => t.ParentTransactionId == parent.Id)
.OrderByDescending(t => t.Date)
.Select(t => (DateTime?)t.Date)
@@ -279,17 +271,17 @@ public class TransactionService(ApplicationDbContext db)
if (newTransactions.Count > 0)
{
- _db.Transactions.AddRange(newTransactions);
- await _db.SaveChangesAsync();
+ db.Transactions.AddRange(newTransactions);
+ await db.SaveChangesAsync();
}
}
internal static DateTime GetNextOccurrence(DateTime current, RecurrenceType recurrence) => recurrence switch
{
- RecurrenceType.Daily => current.AddDays(1),
- RecurrenceType.Weekly => current.AddDays(7),
+ RecurrenceType.Daily => current.AddDays(1),
+ RecurrenceType.Weekly => current.AddDays(7),
RecurrenceType.Monthly => current.AddMonths(1),
- RecurrenceType.Yearly => current.AddYears(1),
- _ => DateTime.MaxValue
+ RecurrenceType.Yearly => current.AddYears(1),
+ _ => DateTime.MaxValue
};
}
diff --git a/BudgetApp.Tests/BudgetApp.Tests.csproj b/BudgetApp.Tests/BudgetApp.Tests.csproj
index 3ae0926..53ac245 100644
--- a/BudgetApp.Tests/BudgetApp.Tests.csproj
+++ b/BudgetApp.Tests/BudgetApp.Tests.csproj
@@ -23,6 +23,7 @@
+
diff --git a/BudgetApp.Tests/Helpers/TestDbContextFactory.cs b/BudgetApp.Tests/Helpers/TestDbContextFactory.cs
index 6418c0c..3377907 100644
--- a/BudgetApp.Tests/Helpers/TestDbContextFactory.cs
+++ b/BudgetApp.Tests/Helpers/TestDbContextFactory.cs
@@ -1,6 +1,7 @@
using Microsoft.Data.Sqlite;
using Microsoft.EntityFrameworkCore;
using BudgetApp.Data;
+using BudgetApp.Data.Entities;
namespace BudgetApp.Tests.Helpers;
diff --git a/BudgetApp.Tests/Models/AccountModelTests.cs b/BudgetApp.Tests/Models/AccountModelTests.cs
index e256c0c..d743bc0 100644
--- a/BudgetApp.Tests/Models/AccountModelTests.cs
+++ b/BudgetApp.Tests/Models/AccountModelTests.cs
@@ -1,4 +1,4 @@
-using BudgetApp.Data;
+using BudgetApp.Data.Entities;
using Xunit;
namespace BudgetApp.Tests.Models;
diff --git a/BudgetApp.Tests/Services/AccountServiceTests.cs b/BudgetApp.Tests/Services/AccountServiceTests.cs
index 2556b01..ed29e0c 100644
--- a/BudgetApp.Tests/Services/AccountServiceTests.cs
+++ b/BudgetApp.Tests/Services/AccountServiceTests.cs
@@ -1,5 +1,5 @@
-using BudgetApp.Data;
-using BudgetApp.Services;
+using BudgetApp.Data.Entities;
+using BudgetApp.Data.Repositories;
using BudgetApp.Tests.Helpers;
using Xunit;
@@ -20,7 +20,7 @@ public class AccountServiceTests
}
using var context = factory.CreateContext();
- var service = new AccountService(context);
+ var service = new AccountRepository(context);
var result = await service.GetAccountsAsync(TestDbContextFactory.TestUserId);
@@ -41,7 +41,7 @@ public class AccountServiceTests
}
using var context = factory.CreateContext();
- var service = new AccountService(context);
+ var service = new AccountRepository(context);
var result = await service.GetAccountsAsync(TestDbContextFactory.TestUserId);
@@ -63,7 +63,7 @@ public class AccountServiceTests
}
using var context = factory.CreateContext();
- var service = new AccountService(context);
+ var service = new AccountRepository(context);
var result = await service.GetAccountAsync(accountId, TestDbContextFactory.TestUserId);
@@ -85,7 +85,7 @@ public class AccountServiceTests
}
using var context = factory.CreateContext();
- var service = new AccountService(context);
+ var service = new AccountRepository(context);
var result = await service.GetAccountAsync(accountId, TestDbContextFactory.OtherUserId);
@@ -97,7 +97,7 @@ public class AccountServiceTests
{
using var factory = new TestDbContextFactory();
using var context = factory.CreateContext();
- var service = new AccountService(context);
+ var service = new AccountRepository(context);
var account = new Account { Name = "New", UserId = TestDbContextFactory.TestUserId, CurrencyCode = "GBP" };
await service.CreateAccountAsync(account);
@@ -120,7 +120,7 @@ public class AccountServiceTests
}
using var context = factory.CreateContext();
- var service = new AccountService(context);
+ var service = new AccountRepository(context);
await service.UpdateBalanceAsync(accountId, TestDbContextFactory.TestUserId, 250m);
using var verifyCtx = factory.CreateContext();
@@ -142,7 +142,7 @@ public class AccountServiceTests
}
using var context = factory.CreateContext();
- var service = new AccountService(context);
+ var service = new AccountRepository(context);
await service.UpdateBalanceAsync(accountId, TestDbContextFactory.OtherUserId, 999m);
using var verifyCtx = factory.CreateContext();
@@ -163,7 +163,7 @@ public class AccountServiceTests
}
using var context = factory.CreateContext();
- var service = new AccountService(context);
+ var service = new AccountRepository(context);
await service.UpdateAccountDetailsAsync(accountId, TestDbContextFactory.TestUserId, "New", "#fff", "USD");
using var verifyCtx = factory.CreateContext();
@@ -187,7 +187,7 @@ public class AccountServiceTests
}
using var context = factory.CreateContext();
- var service = new AccountService(context);
+ var service = new AccountRepository(context);
await service.UpdateAccountDetailsAsync(accountId, TestDbContextFactory.OtherUserId, "Hacked", "#fff", "USD");
using var verifyCtx = factory.CreateContext();
@@ -208,7 +208,7 @@ public class AccountServiceTests
}
using var context = factory.CreateContext();
- var service = new AccountService(context);
+ var service = new AccountRepository(context);
await service.DeleteAccountAsync(accountId, TestDbContextFactory.TestUserId);
using var verifyCtx = factory.CreateContext();
@@ -229,7 +229,7 @@ public class AccountServiceTests
}
using var context = factory.CreateContext();
- var service = new AccountService(context);
+ var service = new AccountRepository(context);
await service.DeleteAccountAsync(accountId, TestDbContextFactory.OtherUserId);
using var verifyCtx = factory.CreateContext();
diff --git a/BudgetApp.Tests/Services/CategoryServiceTests.cs b/BudgetApp.Tests/Services/CategoryServiceTests.cs
index 1536f63..c417cf4 100644
--- a/BudgetApp.Tests/Services/CategoryServiceTests.cs
+++ b/BudgetApp.Tests/Services/CategoryServiceTests.cs
@@ -1,5 +1,5 @@
-using BudgetApp.Data;
-using BudgetApp.Services;
+using BudgetApp.Data.Entities;
+using BudgetApp.Data.Repositories;
using BudgetApp.Tests.Helpers;
using Microsoft.EntityFrameworkCore;
using Xunit;
@@ -21,7 +21,7 @@ public class CategoryServiceTests
}
using var context = factory.CreateContext();
- var service = new CategoryService(context);
+ var service = new CategoryRepository(context);
var result = await service.GetCategoriesAsync(TestDbContextFactory.TestUserId);
@@ -42,7 +42,7 @@ public class CategoryServiceTests
}
using var context = factory.CreateContext();
- var service = new CategoryService(context);
+ var service = new CategoryRepository(context);
var result = await service.GetCategoriesAsync(TestDbContextFactory.TestUserId);
@@ -55,7 +55,7 @@ public class CategoryServiceTests
{
using var factory = new TestDbContextFactory();
using var context = factory.CreateContext();
- var service = new CategoryService(context);
+ var service = new CategoryRepository(context);
await service.EnsureDefaultCategoriesAsync(TestDbContextFactory.TestUserId);
@@ -77,13 +77,13 @@ public class CategoryServiceTests
using (var ctx = factory.CreateContext())
{
- var service = new CategoryService(ctx);
+ var service = new CategoryRepository(ctx);
await service.EnsureDefaultCategoriesAsync(TestDbContextFactory.TestUserId);
}
using (var ctx = factory.CreateContext())
{
- var service = new CategoryService(ctx);
+ var service = new CategoryRepository(ctx);
await service.EnsureDefaultCategoriesAsync(TestDbContextFactory.TestUserId);
}
@@ -111,7 +111,7 @@ public class CategoryServiceTests
}
using var context = factory.CreateContext();
- var service = new CategoryService(context);
+ var service = new CategoryRepository(context);
await service.EnsureDefaultCategoriesAsync(TestDbContextFactory.TestUserId);
using var verifyCtx = factory.CreateContext();
@@ -137,7 +137,7 @@ public class CategoryServiceTests
}
using var context = factory.CreateContext();
- var service = new CategoryService(context);
+ var service = new CategoryRepository(context);
await service.EnsureDefaultCategoriesAsync(TestDbContextFactory.TestUserId);
using var verifyCtx = factory.CreateContext();
@@ -154,13 +154,13 @@ public class CategoryServiceTests
using (var ctx = factory.CreateContext())
{
- var service = new CategoryService(ctx);
+ var service = new CategoryRepository(ctx);
await service.EnsureDefaultCategoriesAsync(TestDbContextFactory.TestUserId);
}
using (var ctx = factory.CreateContext())
{
- var service = new CategoryService(ctx);
+ var service = new CategoryRepository(ctx);
await service.EnsureDefaultCategoriesAsync(TestDbContextFactory.OtherUserId);
}
@@ -174,7 +174,7 @@ public class CategoryServiceTests
{
using var factory = new TestDbContextFactory();
using var context = factory.CreateContext();
- var service = new CategoryService(context);
+ var service = new CategoryRepository(context);
await service.CreateCategoryAsync(new Category
{
@@ -201,7 +201,7 @@ public class CategoryServiceTests
}
using var context = factory.CreateContext();
- var service = new CategoryService(context);
+ var service = new CategoryRepository(context);
await service.UpdateCategoryAsync(categoryId, TestDbContextFactory.TestUserId, "New", "🎯");
using var verifyCtx = factory.CreateContext();
@@ -224,7 +224,7 @@ public class CategoryServiceTests
}
using var context = factory.CreateContext();
- var service = new CategoryService(context);
+ var service = new CategoryRepository(context);
await service.UpdateCategoryAsync(categoryId, TestDbContextFactory.OtherUserId, "Hacked", "💀");
using var verifyCtx = factory.CreateContext();
@@ -245,7 +245,7 @@ public class CategoryServiceTests
}
using var context = factory.CreateContext();
- var service = new CategoryService(context);
+ var service = new CategoryRepository(context);
await service.DeleteCategoryAsync(categoryId, TestDbContextFactory.TestUserId);
using var verifyCtx = factory.CreateContext();
@@ -266,7 +266,7 @@ public class CategoryServiceTests
}
using var context = factory.CreateContext();
- var service = new CategoryService(context);
+ var service = new CategoryRepository(context);
await service.DeleteCategoryAsync(categoryId, TestDbContextFactory.OtherUserId);
using var verifyCtx = factory.CreateContext();
diff --git a/BudgetApp.Tests/Services/DashboardServiceTests.cs b/BudgetApp.Tests/Services/DashboardServiceTests.cs
index 4cadb1e..863cf55 100644
--- a/BudgetApp.Tests/Services/DashboardServiceTests.cs
+++ b/BudgetApp.Tests/Services/DashboardServiceTests.cs
@@ -1,7 +1,7 @@
-using BudgetApp.Data;
+using BudgetApp.Data.Entities;
+using BudgetApp.Data.Repositories;
using BudgetApp.Services;
using BudgetApp.Tests.Helpers;
-using Microsoft.EntityFrameworkCore;
using Xunit;
namespace BudgetApp.Tests.Services;
@@ -181,9 +181,9 @@ public class DashboardServiceTests
}
using var context = factory.CreateContext();
- var transactionService = new TransactionService(context);
- var accountService = new AccountService(context);
- var categoryService = new CategoryService(context);
+ var transactionService = new TransactionRepository(context);
+ var accountService = new AccountRepository(context);
+ var categoryService = new CategoryRepository(context);
var dashboardService = new DashboardService(transactionService, accountService, categoryService);
var result = await dashboardService.GetDashboardDataAsync(TestDbContextFactory.TestUserId);
@@ -214,9 +214,9 @@ public class DashboardServiceTests
}
using var context = factory.CreateContext();
- var transactionService = new TransactionService(context);
- var accountService = new AccountService(context);
- var categoryService = new CategoryService(context);
+ var transactionService = new TransactionRepository(context);
+ var accountService = new AccountRepository(context);
+ var categoryService = new CategoryRepository(context);
var dashboardService = new DashboardService(transactionService, accountService, categoryService);
var result = await dashboardService.GetDashboardDataAsync(TestDbContextFactory.TestUserId);
@@ -243,9 +243,9 @@ public class DashboardServiceTests
}
using var context = factory.CreateContext();
- var transactionService = new TransactionService(context);
- var accountService = new AccountService(context);
- var categoryService = new CategoryService(context);
+ var transactionService = new TransactionRepository(context);
+ var accountService = new AccountRepository(context);
+ var categoryService = new CategoryRepository(context);
var dashboardService = new DashboardService(transactionService, accountService, categoryService);
var from = DateTime.Today.AddDays(-10);
diff --git a/BudgetApp.Tests/Services/TransactionServiceTests.cs b/BudgetApp.Tests/Services/TransactionServiceTests.cs
index 987936e..b1b4156 100644
--- a/BudgetApp.Tests/Services/TransactionServiceTests.cs
+++ b/BudgetApp.Tests/Services/TransactionServiceTests.cs
@@ -1,5 +1,6 @@
using BudgetApp.Data;
-using BudgetApp.Services;
+using BudgetApp.Data.Entities;
+using BudgetApp.Data.Repositories;
using BudgetApp.Tests.Helpers;
using Microsoft.EntityFrameworkCore;
using Xunit;
@@ -32,7 +33,7 @@ public class TransactionServiceTests
}
using var context = factory.CreateContext();
- var service = new TransactionService(context);
+ var service = new TransactionRepository(context);
var result = await service.GetTransactionsAsync(TestDbContextFactory.TestUserId);
Assert.Equal(2, result.Count);
@@ -55,7 +56,7 @@ public class TransactionServiceTests
}
using var context = factory.CreateContext();
- var service = new TransactionService(context);
+ var service = new TransactionRepository(context);
var result = await service.GetTransactionsAsync(TestDbContextFactory.TestUserId);
Assert.Single(result);
@@ -85,7 +86,7 @@ public class TransactionServiceTests
}
using var context = factory.CreateContext();
- var service = new TransactionService(context);
+ var service = new TransactionRepository(context);
var result = await service.GetTransactionsAsync(TestDbContextFactory.TestUserId);
Assert.Single(result);
@@ -113,7 +114,7 @@ public class TransactionServiceTests
}
using var context = factory.CreateContext();
- var service = new TransactionService(context);
+ var service = new TransactionRepository(context);
var result = await service.GetRecentTransactionsAsync(TestDbContextFactory.TestUserId, 10);
Assert.Equal(2, result.Count);
@@ -141,7 +142,7 @@ public class TransactionServiceTests
}
using var context = factory.CreateContext();
- var service = new TransactionService(context);
+ var service = new TransactionRepository(context);
var result = await service.GetRecentTransactionsAsync(TestDbContextFactory.TestUserId, 3);
Assert.Equal(3, result.Count);
@@ -166,7 +167,7 @@ public class TransactionServiceTests
}
using var context = factory.CreateContext();
- var service = new TransactionService(context);
+ var service = new TransactionRepository(context);
var result = await service.GetUpcomingTransactionsAsync(TestDbContextFactory.TestUserId, 10);
Assert.Single(result);
@@ -197,7 +198,7 @@ public class TransactionServiceTests
}
using var context = factory.CreateContext();
- var service = new TransactionService(context);
+ var service = new TransactionRepository(context);
var result = await service.GetTransactionsInRangeAsync(TestDbContextFactory.TestUserId, from, to);
Assert.Equal(3, result.Count);
@@ -229,7 +230,7 @@ public class TransactionServiceTests
}
using var context = factory.CreateContext();
- var service = new TransactionService(context);
+ var service = new TransactionRepository(context);
var result = await service.GetTransactionsByAccountAsync(accountId, TestDbContextFactory.TestUserId);
Assert.Single(result);
@@ -250,7 +251,7 @@ public class TransactionServiceTests
}
using var context = factory.CreateContext();
- var service = new TransactionService(context);
+ var service = new TransactionRepository(context);
var result = await service.GetTransactionsByAccountAsync(accountId, TestDbContextFactory.OtherUserId);
Assert.Empty(result);
@@ -271,7 +272,7 @@ public class TransactionServiceTests
}
using var context = factory.CreateContext();
- var service = new TransactionService(context);
+ var service = new TransactionRepository(context);
await service.CreateTransactionAsync(new Transaction
{
Description = "Groceries",
@@ -296,7 +297,7 @@ public class TransactionServiceTests
}
using var context = factory.CreateContext();
- var service = new TransactionService(context);
+ var service = new TransactionRepository(context);
await service.CreateTransactionAsync(new Transaction
{
Description = "One-off",
@@ -323,7 +324,7 @@ public class TransactionServiceTests
var startDate = new DateTime(2025, 1, 1);
using var context = factory.CreateContext();
- var service = new TransactionService(context);
+ var service = new TransactionRepository(context);
await service.CreateTransactionAsync(new Transaction
{
Description = "Recurring",
@@ -353,7 +354,7 @@ public class TransactionServiceTests
var startDate = new DateTime(2025, 1, 1);
using var context = factory.CreateContext();
- var service = new TransactionService(context);
+ var service = new TransactionRepository(context);
await service.CreateTransactionAsync(new Transaction
{
Description = "Recurring",
@@ -383,7 +384,7 @@ public class TransactionServiceTests
var startDate = new DateTime(2025, 1, 1);
using var context = factory.CreateContext();
- var service = new TransactionService(context);
+ var service = new TransactionRepository(context);
await service.CreateTransactionAsync(new Transaction
{
Description = "Weekly",
@@ -421,7 +422,7 @@ public class TransactionServiceTests
var startDate = new DateTime(2025, 1, 15);
using var context = factory.CreateContext();
- var service = new TransactionService(context);
+ var service = new TransactionRepository(context);
await service.CreateTransactionAsync(new Transaction
{
Description = "Monthly",
@@ -481,7 +482,7 @@ public class TransactionServiceTests
var newDate = DateTime.Today.AddDays(5);
using var context = factory.CreateContext();
- var service = new TransactionService(context);
+ var service = new TransactionRepository(context);
await service.UpdateTransactionAsync(
transactionId, TestDbContextFactory.TestUserId,
"Updated", 99m, newDate, TransactionType.Income,
@@ -520,7 +521,7 @@ public class TransactionServiceTests
}
using var context = factory.CreateContext();
- var service = new TransactionService(context);
+ var service = new TransactionRepository(context);
await service.UpdateTransactionAsync(
transactionId, TestDbContextFactory.OtherUserId,
"Hacked", 999m, DateTime.Today, TransactionType.Income, 1, null);
@@ -548,7 +549,7 @@ public class TransactionServiceTests
}
using var context = factory.CreateContext();
- var service = new TransactionService(context);
+ var service = new TransactionRepository(context);
await service.DeleteTransactionAsync(transactionId, TestDbContextFactory.TestUserId);
using var verifyCtx = factory.CreateContext();
@@ -570,7 +571,7 @@ public class TransactionServiceTests
}
using var context = factory.CreateContext();
- var service = new TransactionService(context);
+ var service = new TransactionRepository(context);
await service.DeleteTransactionAsync(transactionId, TestDbContextFactory.OtherUserId);
using var verifyCtx = factory.CreateContext();
@@ -623,7 +624,7 @@ public class TransactionServiceTests
}
using var context = factory.CreateContext();
- var service = new TransactionService(context);
+ var service = new TransactionRepository(context);
await service.DeleteTransactionSeriesAsync(childId, TestDbContextFactory.TestUserId);
using var verifyCtx = factory.CreateContext();
@@ -672,7 +673,7 @@ public class TransactionServiceTests
}
using var context = factory.CreateContext();
- var service = new TransactionService(context);
+ var service = new TransactionRepository(context);
await service.DeleteTransactionSeriesAsync(childId, TestDbContextFactory.TestUserId);
using var verifyCtx = factory.CreateContext();
@@ -709,7 +710,7 @@ public class TransactionServiceTests
}
using var context = factory.CreateContext();
- var service = new TransactionService(context);
+ var service = new TransactionRepository(context);
await service.DeleteTransactionSeriesAsync(parentId, TestDbContextFactory.TestUserId);
using var verifyCtx = factory.CreateContext();
@@ -731,7 +732,7 @@ public class TransactionServiceTests
}
using var context = factory.CreateContext();
- var service = new TransactionService(context);
+ var service = new TransactionRepository(context);
await service.DeleteTransactionSeriesAsync(transactionId, TestDbContextFactory.OtherUserId);
using var verifyCtx = factory.CreateContext();
@@ -772,7 +773,7 @@ public class TransactionServiceTests
}
using var context = factory.CreateContext();
- var service = new TransactionService(context);
+ var service = new TransactionRepository(context);
await service.MaterializeRecurringTransactionsAsync(TestDbContextFactory.TestUserId, startDate.AddDays(100));
using var verifyCtx = factory.CreateContext();
@@ -808,7 +809,7 @@ public class TransactionServiceTests
}
using var context = factory.CreateContext();
- var service = new TransactionService(context);
+ var service = new TransactionRepository(context);
await service.MaterializeRecurringTransactionsAsync(TestDbContextFactory.TestUserId, startDate.AddDays(365));
using var verifyCtx = factory.CreateContext();
@@ -839,7 +840,7 @@ public class TransactionServiceTests
}
using var context = factory.CreateContext();
- var service = new TransactionService(context);
+ var service = new TransactionRepository(context);
await service.MaterializeRecurringTransactionsAsync(TestDbContextFactory.TestUserId);
using var verifyCtx = factory.CreateContext();
@@ -860,7 +861,7 @@ public class TransactionServiceTests
var current = new DateTime(2025, 6, 15);
var expected = current.AddDays(expectedDays).AddMonths(expectedMonths).AddYears(expectedYears);
- var result = TransactionService.GetNextOccurrence(current, type);
+ var result = TransactionRepository.GetNextOccurrence(current, type);
Assert.Equal(expected, result);
}
@@ -868,7 +869,7 @@ public class TransactionServiceTests
[Fact]
public void GetNextOccurrence_None_ReturnsMaxValue()
{
- var result = TransactionService.GetNextOccurrence(DateTime.Today, RecurrenceType.None);
+ var result = TransactionRepository.GetNextOccurrence(DateTime.Today, RecurrenceType.None);
Assert.Equal(DateTime.MaxValue, result);
}
diff --git a/BudgetApp.slnx b/BudgetApp.slnx
index b4a26d2..828cb22 100644
--- a/BudgetApp.slnx
+++ b/BudgetApp.slnx
@@ -1,4 +1,5 @@
+
diff --git a/BudgetApp/BudgetApp.csproj b/BudgetApp/BudgetApp.csproj
index 3cba5e6..c3894b4 100644
--- a/BudgetApp/BudgetApp.csproj
+++ b/BudgetApp/BudgetApp.csproj
@@ -1,4 +1,4 @@
-
+
net10.0
@@ -12,15 +12,15 @@
+
+
+
+
-
- runtime; build; native; contentfiles; analyzers; buildtransitive
- all
-
diff --git a/BudgetApp/Components/Layout/NavMenu.razor b/BudgetApp/Components/Layout/NavMenu.razor
index a8a16b8..3161194 100644
--- a/BudgetApp/Components/Layout/NavMenu.razor
+++ b/BudgetApp/Components/Layout/NavMenu.razor
@@ -1,4 +1,4 @@
-@inject AccountService AccountService
+@inject IAccountRepository AccountService
@inject AuthenticationStateProvider AuthStateProvider
@inject AppState AppState
@inject NavigationManager Navigation
@@ -70,11 +70,11 @@
- @if (accounts is not null && accounts.Count > 0)
+ @if (_accounts is not null && _accounts.Count > 0)
{
- @foreach (var account in accounts)
+ @foreach (var account in _accounts)
{
}
-
-
-@code {
- private List? accounts;
-
- protected override async Task OnInitializedAsync()
- {
- AppState.OnAccountsChanged += HandleAccountsChanged;
-
- var authState = await AuthStateProvider.GetAuthenticationStateAsync();
- var userId = authState.User.FindFirst(System.Security.Claims.ClaimTypes.NameIdentifier)?.Value;
-
- if (userId is not null)
- {
- accounts = await AccountService.GetAccountsAsync(userId);
- }
- }
-
- private async void HandleAccountsChanged()
- {
- var authState = await AuthStateProvider.GetAuthenticationStateAsync();
- var userId = authState.User.FindFirst(System.Security.Claims.ClaimTypes.NameIdentifier)?.Value;
-
- if (userId is not null)
- {
- accounts = await AccountService.GetAccountsAsync(userId);
- await InvokeAsync(StateHasChanged);
- }
- }
-
- public void Dispose()
- {
- AppState.OnAccountsChanged -= HandleAccountsChanged;
- }
-
- private void NavigateToAccounts()
- {
- Navigation.NavigateTo("accounts");
- }
-}
-
+
\ No newline at end of file
diff --git a/BudgetApp/Components/Layout/NavMenu.razor.cs b/BudgetApp/Components/Layout/NavMenu.razor.cs
new file mode 100644
index 0000000..6cbb597
--- /dev/null
+++ b/BudgetApp/Components/Layout/NavMenu.razor.cs
@@ -0,0 +1,43 @@
+using BudgetApp.Data.Entities;
+
+namespace BudgetApp.Components.Layout;
+
+public partial class NavMenu
+{
+ private List? _accounts;
+
+ protected override async Task OnInitializedAsync()
+ {
+ AppState.OnAccountsChanged += HandleAccountsChanged;
+
+ var authState = await AuthStateProvider.GetAuthenticationStateAsync();
+ var userId = authState.User.FindFirst(System.Security.Claims.ClaimTypes.NameIdentifier)?.Value;
+
+ if (userId is not null)
+ {
+ _accounts = await AccountService.GetAccountsAsync(userId);
+ }
+ }
+
+ private async void HandleAccountsChanged()
+ {
+ var authState = await AuthStateProvider.GetAuthenticationStateAsync();
+ var userId = authState.User.FindFirst(System.Security.Claims.ClaimTypes.NameIdentifier)?.Value;
+
+ if (userId is not null)
+ {
+ _accounts = await AccountService.GetAccountsAsync(userId);
+ await InvokeAsync(StateHasChanged);
+ }
+ }
+
+ public void Dispose()
+ {
+ AppState.OnAccountsChanged -= HandleAccountsChanged;
+ }
+
+ private void NavigateToAccounts()
+ {
+ Navigation.NavigateTo("accounts");
+ }
+}
\ No newline at end of file
diff --git a/BudgetApp/Components/Pages/About.razor b/BudgetApp/Components/Pages/About.razor
index 2f4bc8e..08e91bd 100644
--- a/BudgetApp/Components/Pages/About.razor
+++ b/BudgetApp/Components/Pages/About.razor
@@ -10,10 +10,10 @@
A personal finance dashboard built with Blazor Server.
-@if (links.Count > 0)
+@if (_links.Count > 0)
{
- @foreach (var link in links)
+ @foreach (var link in _links)
{
@((MarkupString)link.Icon) @link.Label
@@ -24,34 +24,4 @@
Sign in or Register
-
-
-@code {
- private record ContactLink(string Label, string Url, string Icon);
- private List links = [];
-
- protected override void OnInitialized()
- {
- var about = Configuration.GetSection("About");
-
- var email = about["Email"];
- if (!string.IsNullOrWhiteSpace(email))
- links.Add(new("Email", $"mailto:{email}", ""));
-
- var discord = about["Discord"];
- if (!string.IsNullOrWhiteSpace(discord))
- links.Add(new("Discord", discord, ""));
-
- var github = about["GitHub"];
- if (!string.IsNullOrWhiteSpace(github))
- links.Add(new("GitHub", github, ""));
-
- var twitter = about["Twitter"];
- if (!string.IsNullOrWhiteSpace(twitter))
- links.Add(new("Twitter", twitter, ""));
-
- var mastodon = about["Mastodon"];
- if (!string.IsNullOrWhiteSpace(mastodon))
- links.Add(new("Mastodon", mastodon, ""));
- }
-}
+
\ No newline at end of file
diff --git a/BudgetApp/Components/Pages/About.razor.cs b/BudgetApp/Components/Pages/About.razor.cs
new file mode 100644
index 0000000..58db54f
--- /dev/null
+++ b/BudgetApp/Components/Pages/About.razor.cs
@@ -0,0 +1,38 @@
+namespace BudgetApp.Components.Pages;
+
+public partial class About
+{
+ private record ContactLink(string Label, string Url, string Icon);
+
+ private readonly List _links = [];
+
+ protected override void OnInitialized()
+ {
+ var about = Configuration.GetSection("About");
+
+ var email = about["Email"];
+ if (!string.IsNullOrWhiteSpace(email))
+ _links.Add(new("Email", $"mailto:{email}",
+ ""));
+
+ var discord = about["Discord"];
+ if (!string.IsNullOrWhiteSpace(discord))
+ _links.Add(new("Discord", discord,
+ ""));
+
+ var github = about["GitHub"];
+ if (!string.IsNullOrWhiteSpace(github))
+ _links.Add(new("GitHub", github,
+ ""));
+
+ var twitter = about["Twitter"];
+ if (!string.IsNullOrWhiteSpace(twitter))
+ _links.Add(new("Twitter", twitter,
+ ""));
+
+ var mastodon = about["Mastodon"];
+ if (!string.IsNullOrWhiteSpace(mastodon))
+ _links.Add(new("Mastodon", mastodon,
+ ""));
+ }
+}
\ No newline at end of file
diff --git a/BudgetApp/Components/Pages/Accounts.razor b/BudgetApp/Components/Pages/Accounts.razor
index 05d37c1..ebbc3c6 100644
--- a/BudgetApp/Components/Pages/Accounts.razor
+++ b/BudgetApp/Components/Pages/Accounts.razor
@@ -1,6 +1,7 @@
@page "/accounts"
+@using BudgetApp.Data.Entities
@attribute [Authorize]
-@inject AccountService AccountService
+@inject IAccountRepository AccountService
@inject AuthenticationStateProvider AuthStateProvider
@inject AppState AppState
@rendermode InteractiveServer
@@ -15,20 +16,20 @@
Add Account
-
+
-
+
-
+
-
+
@foreach (var kvp in Account.CurrencySymbols)
{
@@ -37,7 +38,7 @@
-
+
@@ -47,31 +48,31 @@
-@if (accounts is null)
+@if (_accounts is null)
{
Loading...
}
-else if (accounts.Count == 0)
+else if (_accounts.Count == 0)
{
No accounts yet. Add one above to get started.
}
else
{
- @foreach (var account in accounts)
+ @foreach (var account in _accounts)
{
- @if (editingBalanceAccountId == account.Id)
+ @if (_editingBalanceAccountId == account.Id)
{
@account.GetSymbol()
+ @bind="_editingBalanceValue" />
@@ -131,7 +132,7 @@ else
}
-@if (showDeleteModal && pendingDeleteAccount is not null)
+@if (_showDeleteModal && _pendingDeleteAccount is not null)
{
@@ -141,7 +142,7 @@ else
-
Are you sure you want to delete @pendingDeleteAccount.Name?
+
Are you sure you want to delete @_pendingDeleteAccount.Name?
This will permanently remove the account and all its transactions.
-
+
-
+
+ title="Click to choose an emoji icon" @onclick="OpenAddIconPicker">@(string.IsNullOrEmpty(_formModel.Icon) ? "📁" : _formModel.Icon)
@@ -34,29 +34,29 @@
-@if (categories is null)
+@if (_categories is null)
{
Loading...
}
-else if (categories.Count == 0)
+else if (_categories.Count == 0)
{
No categories yet. Add one above to organise your transactions.
}
else
{
- @foreach (var category in categories)
+ @foreach (var category in _categories)
{
- @if (editingCategoryId == category.Id)
+ @if (_editingCategoryId == category.Id)
{
+ @onclick="OpenEditIconPicker">@(string.IsNullOrEmpty(_editIcon) ? "📁" : _editIcon)
+ style="font-size: 1rem; font-weight: 500;" @bind="_editCategoryName" />
}
else
@@ -73,7 +73,7 @@ else
{
Default
}
- else if (editingCategoryId == category.Id)
+ else if (_editingCategoryId == category.Id)
{
@@ -91,7 +91,7 @@ else
}
-@if (showIconModal)
+@if (_showIconModal)
{
@@ -115,118 +115,9 @@ else
}
@code {
- private List
? categories;
- private CategoryForm formModel = new();
- private string? userId;
// Edit state
- private int? editingCategoryId;
- private string editCategoryName = string.Empty;
- private string editIcon = string.Empty;
// Icon modal state
- private bool showIconModal;
- private string iconPickerTarget = "add"; // "add" or "edit"
- private static readonly string[] AvailableIcons =
- [
- "🛒", "🏠", "💡", "🚗", "🍽️", "🎬", "🏥", "🛡️", "💰", "🏦",
- "📱", "🛍️", "📚", "✈️", "🎁", "🏋️", "🎮", "🎵", "🐾", "☕",
- "🍕", "👶", "💼", "🔧", "📦", "💊", "🎨", "🌿", "🏢", "📰",
- "💳", "🧾", "📋", "🔌", "🚌", "🚀", "🎂", "🧹", "📸", "💎"
- ];
-
- protected override async Task OnInitializedAsync()
- {
- var authState = await AuthStateProvider.GetAuthenticationStateAsync();
- userId = authState.User.FindFirst(System.Security.Claims.ClaimTypes.NameIdentifier)?.Value;
- await LoadCategories();
- }
-
- private async Task LoadCategories()
- {
- if (userId is not null)
- {
- await CategoryService.EnsureDefaultCategoriesAsync(userId);
- categories = await CategoryService.GetCategoriesAsync(userId);
- }
- }
-
- private async Task AddCategory()
- {
- if (userId is null) return;
-
- var category = new Category
- {
- Name = formModel.Name,
- Icon = formModel.Icon,
- UserId = userId
- };
- await CategoryService.CreateCategoryAsync(category);
- formModel = new CategoryForm();
- await LoadCategories();
- }
-
- private void StartCategoryEdit(Category category)
- {
- editingCategoryId = category.Id;
- editCategoryName = category.Name;
- editIcon = category.Icon;
- }
-
- private void CancelCategoryEdit()
- {
- editingCategoryId = null;
- }
-
- private async Task SaveCategoryEdit(int id)
- {
- if (userId is null) return;
-
- await CategoryService.UpdateCategoryAsync(id, userId, editCategoryName, editIcon);
- editingCategoryId = null;
- await LoadCategories();
- }
-
- private async Task DeleteCategory(int id)
- {
- if (userId is null) return;
-
- await CategoryService.DeleteCategoryAsync(id, userId);
- await LoadCategories();
- }
-
- private void OpenAddIconPicker()
- {
- iconPickerTarget = "add";
- showIconModal = true;
- }
-
- private void OpenEditIconPicker()
- {
- iconPickerTarget = "edit";
- showIconModal = true;
- }
-
- private void CloseIconModal() => showIconModal = false;
-
- private void SelectIcon(string emoji)
- {
- if (iconPickerTarget == "add")
- formModel.Icon = emoji;
- else
- editIcon = emoji;
-
- showIconModal = false;
- }
-
- private sealed class CategoryForm
- {
- [System.ComponentModel.DataAnnotations.Required(ErrorMessage = "Category name is required")]
- [System.ComponentModel.DataAnnotations.MaxLength(100)]
- public string Name { get; set; } = string.Empty;
-
- [System.ComponentModel.DataAnnotations.MaxLength(10)]
- public string Icon { get; set; } = "📁";
- }
}
diff --git a/BudgetApp/Components/Pages/Categories.razor.cs b/BudgetApp/Components/Pages/Categories.razor.cs
new file mode 100644
index 0000000..6a9ea0d
--- /dev/null
+++ b/BudgetApp/Components/Pages/Categories.razor.cs
@@ -0,0 +1,118 @@
+
+using BudgetApp.Data.Entities;
+
+namespace BudgetApp.Components.Pages;
+
+public partial class Categories
+{
+ private List? _categories;
+ private CategoryForm _formModel = new();
+ private string? _userId;
+ private int? _editingCategoryId;
+ private string _editCategoryName = string.Empty;
+ private string _editIcon = string.Empty;
+ private bool _showIconModal;
+ private string _iconPickerTarget = "add"; // "add" or "edit"
+
+ private static readonly string[] AvailableIcons =
+ [
+ "??", "??", "??", "??", "???", "??", "??", "???", "??", "??",
+ "??", "???", "??", "??", "??", "???", "??", "??", "??", "?",
+ "??", "??", "??", "??", "??", "??", "??", "??", "??", "??",
+ "??", "??", "??", "??", "??", "??", "??", "??", "??", "??"
+ ];
+
+ protected override async Task OnInitializedAsync()
+ {
+ var authState = await AuthStateProvider.GetAuthenticationStateAsync();
+ _userId = authState.User.FindFirst(System.Security.Claims.ClaimTypes.NameIdentifier)?.Value;
+ await LoadCategories();
+ }
+
+ private async Task LoadCategories()
+ {
+ if (_userId is not null)
+ {
+ await CategoryService.EnsureDefaultCategoriesAsync(_userId);
+ _categories = await CategoryService.GetCategoriesAsync(_userId);
+ }
+ }
+
+ private async Task AddCategory()
+ {
+ if (_userId is null) return;
+
+ var category = new Category
+ {
+ Name = _formModel.Name,
+ Icon = _formModel.Icon,
+ UserId = _userId
+ };
+ await CategoryService.CreateCategoryAsync(category);
+ _formModel = new CategoryForm();
+ await LoadCategories();
+ }
+
+ private void StartCategoryEdit(Category category)
+ {
+ _editingCategoryId = category.Id;
+ _editCategoryName = category.Name;
+ _editIcon = category.Icon;
+ }
+
+ private void CancelCategoryEdit()
+ {
+ _editingCategoryId = null;
+ }
+
+ private async Task SaveCategoryEdit(int id)
+ {
+ if (_userId is null) return;
+
+ await CategoryService.UpdateCategoryAsync(id, _userId, _editCategoryName, _editIcon);
+ _editingCategoryId = null;
+ await LoadCategories();
+ }
+
+ private async Task DeleteCategory(int id)
+ {
+ if (_userId is null) return;
+
+ await CategoryService.DeleteCategoryAsync(id, _userId);
+ await LoadCategories();
+ }
+
+ private void OpenAddIconPicker()
+ {
+ _iconPickerTarget = "add";
+ _showIconModal = true;
+ }
+
+ private void OpenEditIconPicker()
+ {
+ _iconPickerTarget = "edit";
+ _showIconModal = true;
+ }
+
+ private void CloseIconModal() => _showIconModal = false;
+
+ private void SelectIcon(string emoji)
+ {
+ if (_iconPickerTarget == "add")
+ _formModel.Icon = emoji;
+ else
+ _editIcon = emoji;
+
+ _showIconModal = false;
+ }
+
+ private sealed class CategoryForm
+ {
+ [System.ComponentModel.DataAnnotations.Required(ErrorMessage = "Category name is required")]
+ [System.ComponentModel.DataAnnotations.MaxLength(100)]
+ public string Name { get; set; } = string.Empty;
+
+ [System.ComponentModel.DataAnnotations.MaxLength(10)]
+ public string Icon { get; set; } = "??";
+ }
+}
\ No newline at end of file
diff --git a/BudgetApp/Components/Pages/Home.razor b/BudgetApp/Components/Pages/Home.razor
index 38a6f3d..5610017 100644
--- a/BudgetApp/Components/Pages/Home.razor
+++ b/BudgetApp/Components/Pages/Home.razor
@@ -1,10 +1,11 @@
@page "/"
@attribute [Authorize]
@inject DashboardService DashboardService
-@inject TransactionService TransactionService
-@inject SalaryService SalaryService
+@inject ITransactionRepository TransactionService
+@inject ISalaryRepository SalaryService
@inject AuthenticationStateProvider AuthStateProvider
-@inject IJSRuntime JS
+@inject IJSRuntime Js
+@using BudgetApp.Data.Entities
@implements IAsyncDisposable
@rendermode InteractiveServer
@@ -13,11 +14,11 @@
Dashboard
An overview of your balances, recent and upcoming transactions.
-@if (data is null)
+@if (_data is null)
{
Loading...
}
-else if (data.Accounts.Count == 0)
+else if (_data.Accounts.Count == 0)
{
Welcome!
@@ -31,16 +32,16 @@ else
Balance Trend
@@ -51,41 +52,41 @@ else
@* ── Edit / Add Modal ── *@
- @if (showModal)
+ @if (_showModal)
{