Files
Budgeteer/BudgetApp/Components/Pages/Accounts.razor
T
2026-03-02 17:00:53 +00:00

292 lines
12 KiB
Plaintext

@page "/accounts"
@attribute [Authorize]
@inject AccountService AccountService
@inject AuthenticationStateProvider AuthStateProvider
@inject AppState AppState
@rendermode InteractiveServer
<PageTitle>Accounts</PageTitle>
<h3 class="mb-2">Accounts</h3>
<p class="text-muted mb-4">Manage your financial accounts, balances and currencies.</p>
<div class="card mb-4">
<div class="card-header">
<h5 class="mb-0">Add Account</h5>
</div>
<div class="card-body">
<EditForm Model="formModel" OnValidSubmit="AddAccount" FormName="addAccount">
<ToastValidator />
<div class="row g-3 align-items-end">
<div class="col-md-3">
<label class="form-label">Account Name</label>
<InputText @bind-Value="formModel.Name" class="form-control" placeholder="e.g. Current Account" title="A friendly name for this account" />
</div>
<div class="col-md-2">
<label class="form-label">Current Balance</label>
<InputNumber @bind-Value="formModel.CurrentBalance" class="form-control numeric-input" title="The opening balance for this account" />
</div>
<div class="col-md-2">
<label class="form-label">Currency</label>
<InputSelect @bind-Value="formModel.CurrencyCode" class="form-select" title="Currency used for this account">
@foreach (var kvp in Account.CurrencySymbols)
{
<option value="@kvp.Key">@kvp.Key (@kvp.Value)</option>
}
</InputSelect>
</div>
<div class="col-md-2">
<label class="form-label">Colour</label>
<InputText @bind-Value="formModel.Color" type="color" class="form-control form-control-color" title="Colour used to identify this account in charts and lists" />
</div>
<div class="col-md-2">
<button type="submit" class="btn btn-primary w-100">Add</button>
</div>
</div>
</EditForm>
</div>
</div>
@if (accounts is null)
{
<p><em>Loading...</em></p>
}
else if (accounts.Count == 0)
{
<div class="alert alert-info">No accounts yet. Add one above to get started.</div>
}
else
{
<div class="row">
@foreach (var account in accounts)
{
<div class="col-md-4 col-sm-6 mb-3">
<div class="card h-100" style="border-left: 4px solid @account.Color;">
<div class="card-body">
@if (editingAccountId == account.Id)
{
<div class="mb-2">
<label class="form-label form-label-sm mb-1">Name</label>
<input type="text" class="form-control form-control-sm" @bind="editName" />
</div>
<div class="mb-2">
<label class="form-label form-label-sm mb-1">Currency</label>
<select class="form-select form-select-sm" @bind="editCurrencyCode">
@foreach (var kvp in Account.CurrencySymbols)
{
<option value="@kvp.Key">@kvp.Key (@kvp.Value)</option>
}
</select>
</div>
<div class="mb-2">
<label class="form-label form-label-sm mb-1">Colour</label>
<input type="color" class="form-control form-control-color form-control-sm" @bind="editColor" />
</div>
<div class="d-flex gap-1">
<button class="btn btn-success btn-sm" @onclick="() => SaveAccountDetails(account.Id)">Save</button>
<button class="btn btn-secondary btn-sm" @onclick="CancelAccountEdit">Cancel</button>
</div>
}
else
{
<div class="d-flex justify-content-between align-items-start">
<div>
<h5 class="card-title mb-1">@account.Name</h5>
<small class="text-muted">@account.CurrencyCode</small>
</div>
<div class="d-flex gap-1">
<button class="btn btn-outline-primary btn-sm" title="Edit account name, currency or colour" @onclick="() => StartAccountEdit(account)">Edit</button>
<button class="btn btn-outline-danger btn-sm" title="Delete this account and all its transactions" @onclick="() => ConfirmDeleteAccount(account)">Delete</button>
</div>
</div>
@if (editingBalanceAccountId == account.Id)
{
<div class="mt-2">
<div class="input-group input-group-sm">
<span class="input-group-text">@account.GetSymbol()</span>
<input type="number" class="form-control numeric-input" step="0.01"
@bind="editingBalanceValue" />
</div>
<div class="d-flex gap-1 mt-2">
<button class="btn btn-success btn-sm flex-fill" @onclick="() => SaveBalance(account.Id)">Save</button>
<button class="btn btn-outline-secondary btn-sm flex-fill" @onclick="CancelBalanceEdit">Cancel</button>
</div>
</div>
}
else
{
<h4 class="@(account.CurrentBalance >= 0 ? "text-success" : "text-danger") mb-1"
style="cursor: pointer;" title="Click to edit balance"
@onclick="() => StartBalanceEdit(account)">
@account.FormatAmount(account.CurrentBalance)
</h4>
<small class="text-muted">Click balance to adjust</small>
}
}
</div>
</div>
</div>
}
</div>
}
@if (showDeleteModal && pendingDeleteAccount is not null)
{
<div class="modal d-block" tabindex="-1" style="background: rgba(0,0,0,.5);">
<div class="modal-dialog">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title">Delete Account</h5>
<button type="button" class="btn-close" @onclick="CancelDeleteAccount"></button>
</div>
<div class="modal-body">
<p>Are you sure you want to delete <strong>@pendingDeleteAccount.Name</strong>?</p>
<p class="text-danger mb-0">This will permanently remove the account and all its transactions.</p>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-secondary" @onclick="CancelDeleteAccount">Cancel</button>
<button type="button" class="btn btn-danger" @onclick="DeleteAccountConfirmed">Delete</button>
</div>
</div>
</div>
</div>
}
@code {
private List<Account>? accounts;
private AccountForm formModel = new();
private string? userId;
// Balance editing state
private int? editingBalanceAccountId;
private decimal editingBalanceValue;
// Account details editing state
private int? editingAccountId;
private string editName = string.Empty;
private string editColor = "#1b6ec2";
private string editCurrencyCode = "GBP";
// Delete confirmation state
private bool showDeleteModal;
private Account? pendingDeleteAccount;
protected override async Task OnInitializedAsync()
{
var authState = await AuthStateProvider.GetAuthenticationStateAsync();
userId = authState.User.FindFirst(System.Security.Claims.ClaimTypes.NameIdentifier)?.Value;
await LoadAccounts();
}
private async Task LoadAccounts()
{
if (userId is not null)
{
accounts = await AccountService.GetAccountsAsync(userId);
}
}
private async Task AddAccount()
{
if (userId is null) return;
var account = new Account
{
Name = formModel.Name,
CurrentBalance = formModel.CurrentBalance,
Color = formModel.Color,
CurrencyCode = formModel.CurrencyCode,
UserId = userId
};
await AccountService.CreateAccountAsync(account);
formModel = new AccountForm();
await LoadAccounts();
AppState.NotifyAccountsChanged();
}
private void ConfirmDeleteAccount(Account account)
{
pendingDeleteAccount = account;
showDeleteModal = true;
}
private async Task DeleteAccountConfirmed()
{
if (userId is null || pendingDeleteAccount is null) return;
showDeleteModal = false;
await AccountService.DeleteAccountAsync(pendingDeleteAccount.Id, userId);
pendingDeleteAccount = null;
await LoadAccounts();
AppState.NotifyAccountsChanged();
}
private void CancelDeleteAccount()
{
showDeleteModal = false;
pendingDeleteAccount = null;
}
private void StartBalanceEdit(Account account)
{
editingBalanceAccountId = account.Id;
editingBalanceValue = account.CurrentBalance;
}
private void CancelBalanceEdit()
{
editingBalanceAccountId = null;
}
private async Task SaveBalance(int accountId)
{
if (userId is null) return;
await AccountService.UpdateBalanceAsync(accountId, userId, editingBalanceValue);
editingBalanceAccountId = null;
await LoadAccounts();
AppState.NotifyAccountsChanged();
}
private void StartAccountEdit(Account account)
{
editingAccountId = account.Id;
editName = account.Name;
editColor = account.Color;
editCurrencyCode = account.CurrencyCode;
}
private void CancelAccountEdit()
{
editingAccountId = null;
}
private async Task SaveAccountDetails(int accountId)
{
if (userId is null) return;
await AccountService.UpdateAccountDetailsAsync(accountId, userId, editName, editColor, editCurrencyCode);
editingAccountId = null;
await LoadAccounts();
AppState.NotifyAccountsChanged();
}
private sealed class AccountForm
{
[System.ComponentModel.DataAnnotations.Required(ErrorMessage = "Account name is required")]
[System.ComponentModel.DataAnnotations.MaxLength(100)]
public string Name { get; set; } = string.Empty;
public decimal CurrentBalance { get; set; }
[System.ComponentModel.DataAnnotations.Required]
[System.ComponentModel.DataAnnotations.MaxLength(7)]
public string Color { get; set; } = "#1b6ec2";
[System.ComponentModel.DataAnnotations.Required]
[System.ComponentModel.DataAnnotations.MaxLength(3)]
public string CurrencyCode { get; set; } = "GBP";
}
}