291 lines
11 KiB
Plaintext
291 lines
11 KiB
Plaintext
@page "/accounts"
|
|
@attribute [Authorize]
|
|
@inject AccountService AccountService
|
|
@inject AuthenticationStateProvider AuthStateProvider
|
|
@inject AppState AppState
|
|
@rendermode InteractiveServer
|
|
|
|
<PageTitle>Accounts</PageTitle>
|
|
|
|
<h3 class="mb-4">Accounts</h3>
|
|
|
|
<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" />
|
|
</div>
|
|
<div class="col-md-2">
|
|
<label class="form-label">Current Balance</label>
|
|
<InputNumber @bind-Value="formModel.CurrentBalance" class="form-control numeric-input" />
|
|
</div>
|
|
<div class="col-md-2">
|
|
<label class="form-label">Currency</label>
|
|
<InputSelect @bind-Value="formModel.CurrencyCode" class="form-select">
|
|
@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" />
|
|
</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" @onclick="() => StartAccountEdit(account)">Edit</button>
|
|
<button class="btn btn-outline-danger btn-sm" @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";
|
|
}
|
|
}
|