diff --git a/BudgetApp/Components/Layout/NavMenu.razor b/BudgetApp/Components/Layout/NavMenu.razor
index 9ef0852..27f7eb9 100644
--- a/BudgetApp/Components/Layout/NavMenu.razor
+++ b/BudgetApp/Components/Layout/NavMenu.razor
@@ -1,5 +1,7 @@
@inject AccountService AccountService
@inject AuthenticationStateProvider AuthStateProvider
+@inject AppState AppState
+@implements IDisposable
@rendermode InteractiveServer
@@ -74,6 +76,8 @@
protected override async Task OnInitializedAsync()
{
+ AppState.OnAccountsChanged += HandleAccountsChanged;
+
var authState = await AuthStateProvider.GetAuthenticationStateAsync();
var userId = authState.User.FindFirst(System.Security.Claims.ClaimTypes.NameIdentifier)?.Value;
@@ -82,5 +86,22 @@
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;
+ }
}
diff --git a/BudgetApp/Components/Pages/Accounts.razor b/BudgetApp/Components/Pages/Accounts.razor
index f51b7b2..da3cd18 100644
--- a/BudgetApp/Components/Pages/Accounts.razor
+++ b/BudgetApp/Components/Pages/Accounts.razor
@@ -2,6 +2,7 @@
@attribute [Authorize]
@inject AccountService AccountService
@inject AuthenticationStateProvider AuthStateProvider
+@inject AppState AppState
@rendermode InteractiveServer
Accounts
@@ -200,6 +201,7 @@ else
await AccountService.CreateAccountAsync(account);
formModel = new AccountForm();
await LoadAccounts();
+ AppState.NotifyAccountsChanged();
}
private void ConfirmDeleteAccount(Account account)
@@ -216,6 +218,7 @@ else
await AccountService.DeleteAccountAsync(pendingDeleteAccount.Id, userId);
pendingDeleteAccount = null;
await LoadAccounts();
+ AppState.NotifyAccountsChanged();
}
private void CancelDeleteAccount()
@@ -242,6 +245,7 @@ else
await AccountService.UpdateBalanceAsync(accountId, userId, editingBalanceValue);
editingBalanceAccountId = null;
await LoadAccounts();
+ AppState.NotifyAccountsChanged();
}
private void StartAccountEdit(Account account)
@@ -264,6 +268,7 @@ else
await AccountService.UpdateAccountDetailsAsync(accountId, userId, editName, editColor, editCurrencyCode);
editingAccountId = null;
await LoadAccounts();
+ AppState.NotifyAccountsChanged();
}
private sealed class AccountForm
diff --git a/BudgetApp/Components/Pages/Home.razor b/BudgetApp/Components/Pages/Home.razor
index 34686ea..a323421 100644
--- a/BudgetApp/Components/Pages/Home.razor
+++ b/BudgetApp/Components/Pages/Home.razor
@@ -140,29 +140,43 @@ else
+ @if (data.Accounts.Count > 1)
+ {
+
+ Filter:
+ @foreach (var a in data.Accounts)
+ {
+
+ }
+
+ }
@if (activeTab == "recent")
{
- @if (data.RecentTransactions.Count == 0)
+ @if (filteredRecent.Count == 0)
{
No recent transactions.
}
else
{
- @foreach (var t in data.RecentTransactions.Skip((recentPage - 1) * PageSize).Take(PageSize))
+ @foreach (var t in filteredRecent.Skip((recentPage - 1) * PageSize).Take(PageSize))
{
No upcoming transactions.
}
else
{
- @foreach (var t in data.UpcomingTransactions.Skip((upcomingPage - 1) * PageSize).Take(PageSize))
+ @foreach (var t in filteredUpcoming.Skip((upcomingPage - 1) * PageSize).Take(PageSize))
{
hiddenAccountIds = [];
+
+ private List
filteredRecent => data is null ? []
+ : data.RecentTransactions.Where(t => !hiddenAccountIds.Contains(t.AccountId)).ToList();
+
+ private List filteredUpcoming => data is null ? []
+ : data.UpcomingTransactions.Where(t => !hiddenAccountIds.Contains(t.AccountId)).ToList();
+
+ private void ToggleAccountFilter(int accountId)
+ {
+ if (!hiddenAccountIds.Remove(accountId))
+ hiddenAccountIds.Add(accountId);
+ recentPage = 1;
+ upcomingPage = 1;
+ }
+
// Pagination
private int recentPage = 1;
private int upcomingPage = 1;
private const int PageSize = 10;
- private int RecentTotalPages => data is null ? 0 : (int)Math.Ceiling(data.RecentTransactions.Count / (double)PageSize);
- private int UpcomingTotalPages => data is null ? 0 : (int)Math.Ceiling(data.UpcomingTransactions.Count / (double)PageSize);
+ private int RecentTotalPages => (int)Math.Ceiling(filteredRecent.Count / (double)PageSize);
+ private int UpcomingTotalPages => (int)Math.Ceiling(filteredUpcoming.Count / (double)PageSize);
protected override async Task OnInitializedAsync()
{
diff --git a/BudgetApp/Components/Pages/Transactions.razor b/BudgetApp/Components/Pages/Transactions.razor
index d99e4cd..c2c5801 100644
--- a/BudgetApp/Components/Pages/Transactions.razor
+++ b/BudgetApp/Components/Pages/Transactions.razor
@@ -107,7 +107,7 @@ else if (transactions.Count == 0)
else
{
-
@if (showDeleteModal && pendingDeleteTransaction is not null)
@@ -293,6 +316,16 @@ else
private string? userId;
private int? filterAccountId;
+ // Pagination state
+ private int currentPage = 1;
+ private int pageSize = 25;
+ private static readonly int[] pageSizeOptions = [10, 25, 50, 100];
+ private int totalPages => (int)Math.Ceiling(filteredTransactions.Count / (double)pageSize);
+ private List
pagedTransactions => filteredTransactions
+ .Skip((currentPage - 1) * pageSize)
+ .Take(pageSize)
+ .ToList();
+
// Sort state
private string sortColumn = "date";
private bool sortAscending = true;
@@ -325,6 +358,16 @@ else
private void OnAccountFilterChanged(ChangeEventArgs e)
{
filterAccountId = int.TryParse(e.Value?.ToString(), out var id) ? id : null;
+ currentPage = 1;
+ }
+
+ private void OnPageSizeChanged(ChangeEventArgs e)
+ {
+ if (int.TryParse(e.Value?.ToString(), out var size))
+ {
+ pageSize = size;
+ currentPage = 1;
+ }
}
private void SetSort(string column)
@@ -336,6 +379,7 @@ else
sortColumn = column;
sortAscending = true;
}
+ currentPage = 1;
}
private MarkupString SortIndicator(string column)
diff --git a/BudgetApp/Program.cs b/BudgetApp/Program.cs
index 1baa51d..cc55c78 100644
--- a/BudgetApp/Program.cs
+++ b/BudgetApp/Program.cs
@@ -40,6 +40,7 @@ builder.Services.AddScoped();
builder.Services.AddScoped();
builder.Services.AddScoped();
builder.Services.AddScoped();
+builder.Services.AddScoped();
builder.Services.AddCascadingAuthenticationState();
diff --git a/BudgetApp/Services/AppState.cs b/BudgetApp/Services/AppState.cs
new file mode 100644
index 0000000..e49f2a4
--- /dev/null
+++ b/BudgetApp/Services/AppState.cs
@@ -0,0 +1,8 @@
+namespace BudgetApp.Services;
+
+public sealed class AppState
+{
+ public event Action? OnAccountsChanged;
+
+ public void NotifyAccountsChanged() => OnAccountsChanged?.Invoke();
+}
diff --git a/BudgetApp/budgetapp.db-shm b/BudgetApp/budgetapp.db-shm
index 99b6a1c..5e4267e 100644
Binary files a/BudgetApp/budgetapp.db-shm and b/BudgetApp/budgetapp.db-shm differ
diff --git a/BudgetApp/budgetapp.db-wal b/BudgetApp/budgetapp.db-wal
index 3b9f63d..54f2efa 100644
Binary files a/BudgetApp/budgetapp.db-wal and b/BudgetApp/budgetapp.db-wal differ
diff --git a/Dockerfile b/Dockerfile
index 49dec8a..578cd59 100644
--- a/Dockerfile
+++ b/Dockerfile
@@ -23,4 +23,9 @@ LABEL org.opencontainers.image.title="Budgeteer" \
# Ensure the SQLite data directory exists and is writable
RUN mkdir -p /app/data
+# Store the database in the volume-mountable /app/data directory
+ENV ConnectionStrings__DefaultConnection="Data Source=/app/data/budgetapp.db"
+
+VOLUME ["/app/data"]
+
ENTRYPOINT ["dotnet", "BudgetApp.dll"]
diff --git a/README.md b/README.md
index 7fdc5c4..e2eadc2 100644
--- a/README.md
+++ b/README.md
@@ -51,6 +51,9 @@ docker build -t budgeteer .
docker run -d -p 8080:8080 -v budgeteer-data:/app/data --name budgeteer budgeteer
```
+> **Note:** The SQLite database is stored at `/app/data/budgetapp.db` inside the container.
+> Always mount a volume to `/app/data` so your data survives image updates and container replacements.
+
The app will be available at `http://localhost:8080`.
## Project Structure