WIP
This commit is contained in:
1 parent
6f63488c89
commit
64c3eaa473
4 files changed
+86
-39
No files matched your search
@@ -107,31 +107,55 @@ else if (transactions.Count == 0)
|
|||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
<div class="card">
|
<div class="card mb-3">
|
||||||
<div class="card-header d-flex align-items-center gap-2 flex-wrap">
|
<div class="card-header d-flex flex-wrap align-items-center justify-content-between gap-2">
|
||||||
<label class="form-label mb-0 text-nowrap">Account:</label>
|
<h5 class="mb-0">Filters</h5>
|
||||||
<select class="form-select form-select-sm" style="max-width: 200px;" title="Filter transactions by account" @onchange="OnAccountFilterChanged">
|
<div class="d-flex flex-wrap align-items-center gap-2">
|
||||||
<option value="">All Accounts</option>
|
<div class="btn-group btn-group-sm">
|
||||||
@if (accountsList is not null)
|
<button class="btn @(windowPreset == "7d" ? "btn-primary" : "btn-outline-secondary")" title="Show next 7 days" @onclick="() => SetWindow(7)">7d</button>
|
||||||
{
|
<button class="btn @(windowPreset == "30d" ? "btn-primary" : "btn-outline-secondary")" title="Show next 30 days" @onclick="() => SetWindow(30)">30d</button>
|
||||||
@foreach (var a in accountsList)
|
<button class="btn @(windowPreset == "90d" ? "btn-primary" : "btn-outline-secondary")" title="Show next 90 days" @onclick="() => SetWindow(90)">90d</button>
|
||||||
{
|
<button class="btn @(windowPreset == "1y" ? "btn-primary" : "btn-outline-secondary")" title="Show next year" @onclick="() => SetWindow(365)">1y</button>
|
||||||
<option value="@a.Id" selected="@(filterAccountId == a.Id)">@a.Name</option>
|
</div>
|
||||||
}
|
<input type="date" class="form-control form-control-sm" style="width:auto;"
|
||||||
}
|
title="Custom start date" @bind="windowStart" @bind:after="OnWindowDateChanged" />
|
||||||
</select>
|
<span class="text-muted">to</span>
|
||||||
<input type="search" class="form-control form-control-sm" style="max-width: 220px;"
|
<input type="date" class="form-control form-control-sm" style="width:auto;"
|
||||||
title="Search by description, account, category or amount" placeholder="Search..." @bind="searchQuery" @bind:event="oninput" @bind:after="OnSearchChanged" />
|
title="Custom end date" @bind="windowEnd" @bind:after="OnWindowDateChanged" />
|
||||||
<div class="ms-auto d-flex align-items-center gap-2">
|
|
||||||
<label class="form-label mb-0 text-nowrap">Show:</label>
|
|
||||||
<select class="form-select form-select-sm" style="max-width: 90px;" @onchange="OnPageSizeChanged">
|
|
||||||
@foreach (var size in pageSizeOptions)
|
|
||||||
{
|
|
||||||
<option value="@size" selected="@(pageSize == size)">@size</option>
|
|
||||||
}
|
|
||||||
</select>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
<div class="card-body py-2">
|
||||||
|
<div class="row g-2 align-items-center">
|
||||||
|
<div class="col-12 col-sm-auto">
|
||||||
|
<select class="form-select form-select-sm" title="Filter transactions by account" @onchange="OnAccountFilterChanged">
|
||||||
|
<option value="">All Accounts</option>
|
||||||
|
@if (accountsList is not null)
|
||||||
|
{
|
||||||
|
@foreach (var a in accountsList)
|
||||||
|
{
|
||||||
|
<option value="@a.Id" selected="@(filterAccountId == a.Id)">@a.Name</option>
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
<div class="col-12 col-sm">
|
||||||
|
<input type="search" class="form-control form-control-sm"
|
||||||
|
title="Search by description, account, category or amount" placeholder="Search..." @bind="searchQuery" @bind:event="oninput" @bind:after="OnSearchChanged" />
|
||||||
|
</div>
|
||||||
|
<div class="col-auto d-flex align-items-center gap-2">
|
||||||
|
<label class="form-label mb-0 text-nowrap">Show:</label>
|
||||||
|
<select class="form-select form-select-sm" style="width:auto;" @onchange="OnPageSizeChanged">
|
||||||
|
@foreach (var size in pageSizeOptions)
|
||||||
|
{
|
||||||
|
<option value="@size" selected="@(pageSize == size)">@size</option>
|
||||||
|
}
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="card">
|
||||||
<div class="table-responsive">
|
<div class="table-responsive">
|
||||||
<table class="table table-hover mb-0">
|
<table class="table table-hover mb-0">
|
||||||
<thead class="table-light">
|
<thead class="table-light">
|
||||||
@@ -142,8 +166,8 @@ else
|
|||||||
<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" 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 role="button" class="user-select-none" title="Sort by description" @onclick='() => SetSort("description")'>Description @SortIndicator("description")</th>
|
||||||
<th>Account</th>
|
<th>Account</th>
|
||||||
<th>Category</th>
|
<th class="d-none d-md-table-cell">Category</th>
|
||||||
<th>Recurrence</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 role="button" class="user-select-none text-end" title="Sort by amount" @onclick='() => SetSort("amount")'>Amount @SortIndicator("amount")</th>
|
||||||
<th></th>
|
<th></th>
|
||||||
</tr>
|
</tr>
|
||||||
@@ -155,14 +179,14 @@ else
|
|||||||
<td>
|
<td>
|
||||||
<input type="checkbox" class="form-check-input" checked="@selectedIds.Contains(t.Id)" @onchange="() => ToggleSelect(t.Id)" />
|
<input type="checkbox" class="form-check-input" checked="@selectedIds.Contains(t.Id)" @onchange="() => ToggleSelect(t.Id)" />
|
||||||
</td>
|
</td>
|
||||||
<td>@t.Date.ToString("MMM dd, yyyy")</td>
|
<td class="text-nowrap">@t.Date.ToString("MMM dd, yyyy")</td>
|
||||||
<td>@t.Description</td>
|
<td>@t.Description</td>
|
||||||
<td>
|
<td>
|
||||||
<span class="badge" style="background-color: @t.Account.Color;">
|
<span class="badge" style="background-color: @t.Account.Color;">
|
||||||
@t.Account.Name
|
@t.Account.Name
|
||||||
</span>
|
</span>
|
||||||
</td>
|
</td>
|
||||||
<td>
|
<td class="d-none d-md-table-cell">
|
||||||
@if (t.Category is not null)
|
@if (t.Category is not null)
|
||||||
{
|
{
|
||||||
@if (!string.IsNullOrEmpty(t.Category.Icon))
|
@if (!string.IsNullOrEmpty(t.Category.Icon))
|
||||||
@@ -176,7 +200,7 @@ else
|
|||||||
<span>—</span>
|
<span>—</span>
|
||||||
}
|
}
|
||||||
</td>
|
</td>
|
||||||
<td>
|
<td class="d-none d-md-table-cell">
|
||||||
@if (t.RecurrenceType != RecurrenceType.None)
|
@if (t.RecurrenceType != RecurrenceType.None)
|
||||||
{
|
{
|
||||||
<span class="badge bg-info">🔁 @t.RecurrenceType</span>
|
<span class="badge bg-info">🔁 @t.RecurrenceType</span>
|
||||||
@@ -186,16 +210,14 @@ else
|
|||||||
<span class="badge bg-info">🔁 @(t.ParentTransaction?.RecurrenceType.ToString() ?? "Recurring")</span>
|
<span class="badge bg-info">🔁 @(t.ParentTransaction?.RecurrenceType.ToString() ?? "Recurring")</span>
|
||||||
}
|
}
|
||||||
</td>
|
</td>
|
||||||
<td class="text-end @(t.Type == TransactionType.Income ? "text-success" : "text-danger") fw-semibold">
|
<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)
|
@(t.Type == TransactionType.Income ? "+" : "-")@t.Account.FormatAmount(t.Amount)
|
||||||
</td>
|
</td>
|
||||||
<td class="text-end">
|
<td class="text-end text-nowrap">
|
||||||
<button class="btn btn-outline-primary btn-sm" @onclick="() => OpenEditModal(t)">
|
<div class="btn-group btn-group-sm">
|
||||||
Edit
|
<button class="btn btn-outline-primary" @onclick="() => OpenEditModal(t)">Edit</button>
|
||||||
</button>
|
<button class="btn btn-outline-danger" @onclick="() => ConfirmDelete(t)">Delete</button>
|
||||||
<button class="btn btn-outline-danger btn-sm" @onclick="() => ConfirmDelete(t)">
|
</div>
|
||||||
Delete
|
|
||||||
</button>
|
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
}
|
}
|
||||||
@@ -204,7 +226,7 @@ else
|
|||||||
</div>
|
</div>
|
||||||
@if (totalPages > 1)
|
@if (totalPages > 1)
|
||||||
{
|
{
|
||||||
<div class="card-footer d-flex justify-content-between align-items-center">
|
<div class="card-footer d-flex flex-wrap justify-content-between align-items-center gap-2">
|
||||||
<button class="btn btn-sm btn-outline-secondary" disabled="@(currentPage <= 1 ? true : (bool?)null)" @onclick="() => currentPage--">← Prev</button>
|
<button class="btn btn-sm btn-outline-secondary" disabled="@(currentPage <= 1 ? true : (bool?)null)" @onclick="() => currentPage--">← Prev</button>
|
||||||
<div class="d-flex align-items-center gap-3">
|
<div class="d-flex align-items-center gap-3">
|
||||||
@if (selectedIds.Count > 0)
|
@if (selectedIds.Count > 0)
|
||||||
@@ -220,7 +242,7 @@ else
|
|||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
<div class="card-footer d-flex justify-content-between align-items-center">
|
<div class="card-footer d-flex flex-wrap justify-content-between align-items-center gap-2">
|
||||||
<small class="text-muted">@filteredTransactions.Count transactions</small>
|
<small class="text-muted">@filteredTransactions.Count transactions</small>
|
||||||
@if (selectedIds.Count > 0)
|
@if (selectedIds.Count > 0)
|
||||||
{
|
{
|
||||||
@@ -364,6 +386,11 @@ else
|
|||||||
private int? filterAccountId;
|
private int? filterAccountId;
|
||||||
private string searchQuery = string.Empty;
|
private string searchQuery = string.Empty;
|
||||||
|
|
||||||
|
// Time window state
|
||||||
|
private DateTime windowStart = DateTime.Today;
|
||||||
|
private DateTime windowEnd = DateTime.Today.AddDays(30);
|
||||||
|
private string? windowPreset = "30d";
|
||||||
|
|
||||||
// Pagination state
|
// Pagination state
|
||||||
private int currentPage = 1;
|
private int currentPage = 1;
|
||||||
private int pageSize = 25;
|
private int pageSize = 25;
|
||||||
@@ -382,7 +409,7 @@ else
|
|||||||
{
|
{
|
||||||
get
|
get
|
||||||
{
|
{
|
||||||
var list = (transactions ?? []).Where(t => t.Date.Date >= DateTime.Today);
|
var list = (transactions ?? []).Where(t => t.Date.Date >= windowStart.Date && t.Date.Date <= windowEnd.Date);
|
||||||
if (filterAccountId is not null)
|
if (filterAccountId is not null)
|
||||||
list = list.Where(t => t.AccountId == filterAccountId);
|
list = list.Where(t => t.AccountId == filterAccountId);
|
||||||
|
|
||||||
@@ -421,6 +448,20 @@ else
|
|||||||
|
|
||||||
private void OnSearchChanged() => currentPage = 1;
|
private void OnSearchChanged() => currentPage = 1;
|
||||||
|
|
||||||
|
private void SetWindow(int days)
|
||||||
|
{
|
||||||
|
windowPreset = days switch { 7 => "7d", 30 => "30d", 90 => "90d", 365 => "1y", _ => null };
|
||||||
|
windowStart = DateTime.Today;
|
||||||
|
windowEnd = DateTime.Today.AddDays(days);
|
||||||
|
currentPage = 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void OnWindowDateChanged()
|
||||||
|
{
|
||||||
|
windowPreset = null;
|
||||||
|
currentPage = 1;
|
||||||
|
}
|
||||||
|
|
||||||
private void OnPageSizeChanged(ChangeEventArgs e)
|
private void OnPageSizeChanged(ChangeEventArgs e)
|
||||||
{
|
{
|
||||||
if (int.TryParse(e.Value?.ToString(), out var size))
|
if (int.TryParse(e.Value?.ToString(), out var size))
|
||||||
|
|||||||
@@ -0,0 +1,6 @@
|
|||||||
|
@media (max-width: 767.98px) {
|
||||||
|
::deep .table > :not(caption) > * > * {
|
||||||
|
padding: 0.3rem 0.4rem;
|
||||||
|
font-size: 0.8125rem;
|
||||||
|
}
|
||||||
|
}
|
||||||
Binary file not shown.
Binary file not shown.
Reference in new issue
Block a user