Refactor. Add data layer
This commit is contained in:
1 parent
3b7e4e6ec5
commit
b6b794f0b4
82 files changed
+2930
-2811
No files matched your search
@@ -1,7 +1,8 @@
|
||||
@page "/salary"
|
||||
@using BudgetApp.Data.Entities
|
||||
@attribute [Authorize]
|
||||
@inject SalaryService SalaryService
|
||||
@inject AccountService AccountService
|
||||
@inject ISalaryRepository SalaryService
|
||||
@inject IAccountRepository AccountService
|
||||
@inject AuthenticationStateProvider AuthStateProvider
|
||||
@rendermode InteractiveServer
|
||||
|
||||
@@ -15,7 +16,7 @@
|
||||
<h5 class="mb-0">Add Income Source</h5>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
@if (accountsList is not null && accountsList.Count == 0)
|
||||
@if (_accountsList is not null && _accountsList.Count == 0)
|
||||
{
|
||||
<div class="alert alert-warning mb-0">
|
||||
You need to <a href="/accounts">create an account</a> before adding a salary.
|
||||
@@ -23,23 +24,23 @@
|
||||
}
|
||||
else
|
||||
{
|
||||
<EditForm Model="formModel" OnValidSubmit="AddSalary" FormName="addSalary">
|
||||
<EditForm Model="_formModel" OnValidSubmit="AddSalary" FormName="addSalary">
|
||||
<ToastValidator />
|
||||
<div class="row g-3">
|
||||
<div class="col-md-3">
|
||||
<label class="form-label">Name</label>
|
||||
<InputText @bind-Value="formModel.Name" class="form-control" placeholder="e.g. Main Job" />
|
||||
<InputText @bind-Value="_formModel.Name" class="form-control" placeholder="e.g. Main Job" />
|
||||
</div>
|
||||
<div class="col-md-2">
|
||||
<label class="form-label">Amount</label>
|
||||
<InputNumber @bind-Value="formModel.Amount" class="form-control numeric-input" />
|
||||
<InputNumber @bind-Value="_formModel.Amount" class="form-control numeric-input" />
|
||||
</div>
|
||||
<div class="col-md-2">
|
||||
<label class="form-label">Account</label>
|
||||
<InputSelect @bind-Value="formModel.AccountId" class="form-select">
|
||||
@if (accountsList is not null)
|
||||
<InputSelect @bind-Value="_formModel.AccountId" class="form-select">
|
||||
@if (_accountsList is not null)
|
||||
{
|
||||
@foreach (var account in accountsList)
|
||||
@foreach (var account in _accountsList)
|
||||
{
|
||||
<option value="@account.Id">@account.Name</option>
|
||||
}
|
||||
@@ -48,7 +49,7 @@
|
||||
</div>
|
||||
<div class="col-md-2">
|
||||
<label class="form-label">Recurrence</label>
|
||||
<InputSelect @bind-Value="formModel.RecurrenceType" class="form-select">
|
||||
<InputSelect @bind-Value="_formModel.RecurrenceType" class="form-select">
|
||||
<option value="@RecurrenceType.Weekly">Weekly</option>
|
||||
<option value="@RecurrenceType.Monthly">Monthly</option>
|
||||
<option value="@RecurrenceType.Yearly">Yearly</option>
|
||||
@@ -56,7 +57,7 @@
|
||||
</div>
|
||||
<div class="col-md-3">
|
||||
<label class="form-label">First Payday</label>
|
||||
<InputDate @bind-Value="formModel.StartDate" class="form-control" title="The date of the first payment — the interval repeats from here indefinitely" />
|
||||
<InputDate @bind-Value="_formModel.StartDate" class="form-control" title="The date of the first payment — the interval repeats from here indefinitely" />
|
||||
</div>
|
||||
<div class="col-md-12 col-lg-auto d-flex align-items-end">
|
||||
<button type="submit" class="btn btn-primary w-100">Add</button>
|
||||
@@ -67,11 +68,11 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@if (salaries is null)
|
||||
@if (_salaries is null)
|
||||
{
|
||||
<p><em>Loading...</em></p>
|
||||
}
|
||||
else if (salaries.Count == 0)
|
||||
else if (_salaries.Count == 0)
|
||||
{
|
||||
<div class="alert alert-info">No income sources yet. Add one above to start tracking your salary.</div>
|
||||
}
|
||||
@@ -94,7 +95,7 @@ else
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
@foreach (var s in salaries)
|
||||
@foreach (var s in _salaries)
|
||||
{
|
||||
<tr>
|
||||
<td class="fw-semibold">💰 @s.Name</td>
|
||||
@@ -108,7 +109,7 @@ else
|
||||
<span class="badge bg-info text-nowrap">🔁 @s.GetPaydaySummary()</span>
|
||||
</td>
|
||||
<td class="text-nowrap">
|
||||
@if (nextPaydates.TryGetValue(s.Id, out var next) && next.HasValue)
|
||||
@if (_nextPaydates.TryGetValue(s.Id, out var next) && next.HasValue)
|
||||
{
|
||||
<span>@next.Value.ToString("MMM dd, yyyy")</span>
|
||||
}
|
||||
@@ -126,12 +127,12 @@ else
|
||||
</table>
|
||||
</div>
|
||||
<div class="card-footer">
|
||||
<small class="text-muted">@salaries.Count income source@(salaries.Count == 1 ? "" : "s") · Transactions are generated automatically and appear on the <a href="/transactions">Transactions</a> page.</small>
|
||||
<small class="text-muted">@_salaries.Count income source@(_salaries.Count == 1 ? "" : "s") · Transactions are generated automatically and appear on the <a href="/transactions">Transactions</a> page.</small>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
|
||||
@if (showDeleteModal && pendingDeleteSalary is not null)
|
||||
@if (_showDeleteModal && _pendingDeleteSalary is not null)
|
||||
{
|
||||
<div class="modal d-block" tabindex="-1" style="background: rgba(0,0,0,.5);">
|
||||
<div class="modal-dialog">
|
||||
@@ -141,7 +142,7 @@ else
|
||||
<button type="button" class="btn-close" @onclick="CancelDelete"></button>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<p>Are you sure you want to delete <strong>@pendingDeleteSalary.Name</strong>?</p>
|
||||
<p>Are you sure you want to delete <strong>@_pendingDeleteSalary.Name</strong>?</p>
|
||||
<p>All future unpaid salary transactions will be removed. Past transactions remain in your history.</p>
|
||||
<p class="text-danger mb-0">This action cannot be undone.</p>
|
||||
</div>
|
||||
@@ -155,110 +156,7 @@ else
|
||||
}
|
||||
|
||||
@code {
|
||||
private List<BudgetApp.Data.Salary>? salaries;
|
||||
private List<Account>? accountsList;
|
||||
private Dictionary<int, DateTime?> nextPaydates = [];
|
||||
private string? userId;
|
||||
|
||||
private SalaryForm formModel = new();
|
||||
|
||||
protected override async Task OnInitializedAsync()
|
||||
{
|
||||
var authState = await AuthStateProvider.GetAuthenticationStateAsync();
|
||||
userId = authState.User.FindFirst(System.Security.Claims.ClaimTypes.NameIdentifier)?.Value;
|
||||
await LoadData();
|
||||
}
|
||||
|
||||
private async Task LoadData()
|
||||
{
|
||||
if (userId is null) return;
|
||||
|
||||
var windowEnd = DateTime.Today.AddDays(90);
|
||||
await SalaryService.MaterializeSalaryTransactionsAsync(userId, windowEnd);
|
||||
|
||||
salaries = await SalaryService.GetSalariesAsync(userId);
|
||||
accountsList = await AccountService.GetAccountsAsync(userId);
|
||||
|
||||
nextPaydates.Clear();
|
||||
foreach (var sal in salaries)
|
||||
{
|
||||
nextPaydates[sal.Id] = await SalaryService.GetNextPaydateAsync(sal.Id);
|
||||
}
|
||||
|
||||
if (accountsList.Count > 0 && formModel.AccountId == 0)
|
||||
{
|
||||
formModel.AccountId = accountsList[0].Id;
|
||||
}
|
||||
}
|
||||
|
||||
private async Task AddSalary()
|
||||
{
|
||||
if (userId is null) return;
|
||||
|
||||
var entry = new BudgetApp.Data.Salary
|
||||
{
|
||||
Name = formModel.Name,
|
||||
Amount = formModel.Amount,
|
||||
AccountId = formModel.AccountId,
|
||||
RecurrenceType = formModel.RecurrenceType,
|
||||
StartDate = formModel.StartDate,
|
||||
UserId = userId
|
||||
};
|
||||
|
||||
await SalaryService.CreateSalaryAsync(entry, DateTime.Today.AddDays(90));
|
||||
|
||||
formModel = new SalaryForm
|
||||
{
|
||||
AccountId = accountsList?.FirstOrDefault()?.Id ?? 0
|
||||
};
|
||||
|
||||
await LoadData();
|
||||
}
|
||||
|
||||
// Delete state
|
||||
private bool showDeleteModal;
|
||||
private BudgetApp.Data.Salary? pendingDeleteSalary;
|
||||
|
||||
private void ConfirmDelete(BudgetApp.Data.Salary s)
|
||||
{
|
||||
pendingDeleteSalary = s;
|
||||
showDeleteModal = true;
|
||||
}
|
||||
|
||||
private void CancelDelete()
|
||||
{
|
||||
showDeleteModal = false;
|
||||
pendingDeleteSalary = null;
|
||||
}
|
||||
|
||||
private async Task ExecuteDelete()
|
||||
{
|
||||
if (userId is null || pendingDeleteSalary is null) return;
|
||||
showDeleteModal = false;
|
||||
await SalaryService.DeleteSalaryAsync(pendingDeleteSalary.Id, userId);
|
||||
pendingDeleteSalary = null;
|
||||
await LoadData();
|
||||
}
|
||||
|
||||
private sealed class SalaryForm : System.ComponentModel.DataAnnotations.IValidatableObject
|
||||
{
|
||||
[System.ComponentModel.DataAnnotations.Required(ErrorMessage = "Name is required")]
|
||||
[System.ComponentModel.DataAnnotations.MaxLength(200)]
|
||||
public string Name { get; set; } = string.Empty;
|
||||
|
||||
[System.ComponentModel.DataAnnotations.Range(0.01, double.MaxValue, ErrorMessage = "Amount must be greater than zero")]
|
||||
public decimal Amount { get; set; }
|
||||
|
||||
public int AccountId { get; set; }
|
||||
|
||||
public RecurrenceType RecurrenceType { get; set; } = RecurrenceType.Monthly;
|
||||
|
||||
public DateTime StartDate { get; set; } = DateTime.Today;
|
||||
|
||||
public IEnumerable<System.ComponentModel.DataAnnotations.ValidationResult> Validate(
|
||||
System.ComponentModel.DataAnnotations.ValidationContext validationContext)
|
||||
{
|
||||
yield break;
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user