This commit is contained in:
rossmill76 committed 2026-03-23 15:05:57 +00:00
1 parent 2bfe914c8f
commit 4281924ee5
16 files changed
+369 -76

No files matched your search

+56 -47
View File
@@ -6,6 +6,7 @@
@inject ICategoryRepository CategoryService
@inject ISalaryRepository SalaryService
@inject AuthenticationStateProvider AuthStateProvider
@inject IJSRuntime JS
@rendermode InteractiveServer
<PageTitle>Transactions</PageTitle>
@@ -29,26 +30,26 @@
<EditForm Model="_formModel" OnValidSubmit="AddTransaction" FormName="addTransaction">
<ToastValidator />
<div class="row g-3">
<div class="col-md-4">
<div class="col-12 col-sm-6 col-md-4">
<label class="form-label">Description</label>
<InputText @bind-Value="_formModel.Description" class="form-control" placeholder="e.g. Food shopping" title="A short description of the transaction" />
</div>
<div class="col-md-2">
<div class="col-6 col-sm-4 col-md-2">
<label class="form-label">Amount</label>
<InputNumber @bind-Value="_formModel.Amount" class="form-control numeric-input" title="Enter a positive amount — the type determines income or expense" />
</div>
<div class="col-md-2">
<div class="col-6 col-sm-4 col-md-2">
<label class="form-label">Date</label>
<InputDate @bind-Value="_formModel.Date" class="form-control" />
</div>
<div class="col-md-2">
<div class="col-6 col-sm-4 col-md-2">
<label class="form-label">Type</label>
<InputSelect @bind-Value="_formModel.Type" class="form-select">
<option value="@TransactionType.Expense">Expense</option>
<option value="@TransactionType.Income">Income</option>
</InputSelect>
</div>
<div class="col-md-2">
<div class="col-6 col-sm-4 col-md-2">
<label class="form-label">Account</label>
<InputSelect @bind-Value="_formModel.AccountId" class="form-select">
@if (_accountsList is not null)
@@ -60,7 +61,7 @@
}
</InputSelect>
</div>
<div class="col-md-4">
<div class="col-12 col-sm-6 col-md-4">
<label class="form-label">Category</label>
<InputSelect @bind-Value="_formModel.CategoryId" class="form-select">
<option value="">— None —</option>
@@ -73,7 +74,7 @@
}
</InputSelect>
</div>
<div class="col-md-2">
<div class="col-6 col-sm-4 col-md-2">
<label class="form-label">Recurrence</label>
<InputSelect @bind-Value="_formModel.RecurrenceType" class="form-select" title="Set a recurring schedule — future occurrences are generated automatically">
<option value="@RecurrenceType.None">None</option>
@@ -85,12 +86,12 @@
</div>
@if (_formModel.RecurrenceType != RecurrenceType.None)
{
<div class="col-md-2">
<div class="col-6 col-sm-4 col-md-2">
<label class="form-label">End Date <small class="text-muted">(max 1yr)</small></label>
<InputDate @bind-Value="_formModel.RecurrenceEndDate" class="form-control" max="@_formModel.Date.AddYears(1).ToString("yyyy-MM-dd")" />
</div>
}
<div class="col-md-2 d-flex align-items-end">
<div class="col-12 col-md-2 d-flex align-items-end">
<button type="submit" class="btn btn-primary w-100">Add</button>
</div>
</div>
@@ -153,6 +154,9 @@ else
}
</select>
</div>
<div class="col-auto">
<button class="btn btn-sm btn-outline-success" @onclick="ExportCsv" title="Export filtered transactions to CSV">⬇ CSV</button>
</div>
</div>
</div>
</div>
@@ -162,23 +166,22 @@ else
<table class="table table-hover mb-0">
<thead class="table-light">
<tr>
<th style="width: 2rem;">
<th style="width:2rem;">
<input type="checkbox" class="form-check-input" title="Select all transactions on this page" checked="@IsAllPageSelected()" @onchange="ToggleSelectAll" />
</th>
<th role="button" class="user-select-none" title="Sort by date" @onclick='() => SetSort("date")'>Date @SortIndicator("date")</th>
<th role="button" class="user-select-none text-nowrap" style="width:7.5rem;" title="Sort by date" @onclick='() => SetSort("date")'>Date @SortIndicator("date")</th>
<th role="button" class="user-select-none" title="Sort by description" @onclick='() => SetSort("description")'>Description @SortIndicator("description")</th>
<th>Account</th>
<th class="d-none d-md-table-cell">Category</th>
<th class="d-none d-md-table-cell">Recurrence</th>
<th role="button" class="user-select-none text-end" title="Sort by amount" @onclick='() => SetSort("amount")'>Amount @SortIndicator("amount")</th>
<th></th>
<th style="width:8rem;">Account</th>
<th class="d-none d-md-table-cell" style="width:9rem;">Category</th>
<th class="d-none d-md-table-cell" style="width:7rem;">Recurrence</th>
<th role="button" class="user-select-none text-end text-nowrap" style="width:7rem;" title="Sort by amount" @onclick='() => SetSort("amount")'>Amount @SortIndicator("amount")</th>
</tr>
</thead>
<tbody>
@foreach (var t in PagedTransactions)
{
<tr>
<td>
<tr class="row-clickable" @onclick="() => OpenEditModal(t)">
<td @onclick:stopPropagation="true">
@if (t.SalaryId is null)
{
<input type="checkbox" class="form-check-input" checked="@_selectedIds.Contains(t.Id)" @onchange="() => ToggleSelect(t.Id)" />
@@ -195,10 +198,6 @@ else
{
<span class="badge bg-primary ms-1" style="font-size: 0.7rem;">💰 Salary</span>
}
@if (t.IsPaid && t.Date.Date > DateTime.Today)
{
<span class="badge bg-success ms-1" style="font-size: 0.7rem;">✓ Paid</span>
}
</td>
<td>
<span class="badge" style="background-color: @t.Account.Color;">
@@ -228,31 +227,16 @@ else
{
<span class="badge bg-info">🔁 @(t.ParentTransaction?.RecurrenceType.ToString() ?? "Recurring")</span>
}
else
{
<span class="text-muted" style="font-size:0.8rem;">None</span>
}
</td>
<td class="text-end text-nowrap @(t.Type == TransactionType.Income ? "text-success" : "text-danger") fw-semibold">
@(t.Type == TransactionType.Income ? "+" : "-")@t.Account.FormatAmount(t.Amount)
</td>
<td class="text-end text-nowrap">
<div class="btn-group btn-group-sm">
@if (t.Date.Date > DateTime.Today)
{
<button class="btn @(t.IsPaid ? "btn-success" : "btn-outline-success")"
title="@(t.IsPaid ? "Mark as unpaid" : "Mark as paid")"
@onclick="() => TogglePaid(t)">✓</button>
}
<button class="btn btn-outline-primary" @onclick="() => OpenEditModal(t)">Edit</button>
@if (t.SalaryId is null)
{
<button class="btn btn-outline-danger" @onclick="() => ConfirmDelete(t)">Delete</button>
}
else
{
<a href="/salary" class="btn btn-outline-secondary" title="Salary transactions are managed on the Salary page">🔒</a>
}
</div>
</td>
</tr>
}
</tr>
}
</tbody>
</table>
</div>
@@ -391,16 +375,41 @@ else
}
</InputSelect>
</div>
@if (_editIsPartOfSeries)
@if (_editingTransaction?.SalaryId is not null)
{
<div class="alert alert-warning mb-3">
This is a salary transaction. To modify it, use the <a href="/salary">Salary page</a>.
</div>
}
else if (_editIsPartOfSeries)
{
<div class="alert alert-info mb-0">
<i class="bi bi-info-circle"></i> Changes will apply to this and all future occurrences in the series.
</div>
}
</div>
<div class="modal-footer">
<button type="button" class="btn btn-secondary" @onclick="CloseEditModal">Cancel</button>
<button type="submit" class="btn btn-primary">Save</button>
<div class="modal-footer justify-content-between">
<div class="d-flex gap-2">
@if (_editingTransaction is not null && _editingTransaction.SalaryId is null)
{
<button type="button" class="btn btn-outline-danger"
@onclick="() => { var tx = _editingTransaction!; CloseEditModal(); ConfirmDelete(tx); }">Delete</button>
}
@if (_editingTransaction is not null && _editingTransaction.Date.Date > DateTime.Today)
{
<button type="button" class="btn @(_editingTransaction.IsPaid ? "btn-success" : "btn-outline-success")"
@onclick="async () => { var tx = _editingTransaction!; CloseEditModal(); await TogglePaid(tx); }">
@(_editingTransaction.IsPaid ? "✓ Paid" : "Mark Paid")
</button>
}
</div>
<div class="d-flex gap-2">
<button type="button" class="btn btn-secondary" @onclick="CloseEditModal">Cancel</button>
@if (_editingTransaction?.SalaryId is null)
{
<button type="submit" class="btn btn-primary">Save</button>
}
</div>
</div>
</EditForm>
</div>