Skip to content
Open
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
227 changes: 226 additions & 1 deletion BTCPayApp.UI/Components/InvoiceList.razor
Original file line number Diff line number Diff line change
Expand Up @@ -4,11 +4,132 @@
@inject IAccountManager AccountManager
@inject IDispatcher Dispatcher
<div @attributes="InputAttributes" class="@CssClass">
@{
var archivedCount = Invoices?.Count(i => i.Archived) ?? 0;
var shownArchivedCount = FilteredInvoices?.Count(i => i.Archived) ?? 0;
}
<div class="d-none">Archived: DB=@archivedCount, Shown=@shownArchivedCount</div>
@if (Invoices is not null)
{
@if (Invoices.Any())
{
@foreach (var i in Invoices)
<div class="d-flex align-items-center justify-content-between mb-3 flex-wrap gap-3">
<input type="text"
class="form-control border-0 text-white"
placeholder="Search invoices..."
@bind="_searchText"
@bind:event="oninput" />

<div class="d-flex gap-3">
<div class="dropdown">
<button class="btn btn-outline-success rounded-pill" type="button" data-bs-toggle="dropdown">
@GetSelectedStatusText()
</button>
<ul class="dropdown-menu" style="min-width: 200px; padding: 0; margin: 0;">
@foreach (var status in Enum.GetValues<InvoiceStatus>())
{
bool isSelected = _selectedStatuses.Contains(status);
<li style="margin: 0; padding: 0; list-style: none;">
<div class="form-check" style="margin: 0; padding: 0;">
<input class="form-check-input d-none"
type="checkbox"
id="status-@status"
@onchange="() => ToggleStatus(status)" />
<label class="form-check-label w-100 d-block @(isSelected ? "bg-light" : "")"
for="status-@status"
style="cursor: pointer; margin: 0; padding: 12px 16px; display: block;">
@status
</label>
</div>
</li>
}
<hr class="dropdown-divider mx-3" />
@foreach (var exceptionStatus in Enum.GetValues<InvoiceExceptionStatus>()
.Where(s => s != InvoiceExceptionStatus.None &&
s != InvoiceExceptionStatus.Marked))
{
bool isSelected = _selectedExceptionStatuses.Contains(exceptionStatus);
string displayName = exceptionStatus switch
{
InvoiceExceptionStatus.PaidLate => "Settled Late",
InvoiceExceptionStatus.PaidPartial => "Settled Partial",
InvoiceExceptionStatus.PaidOver => "Settled Over",
_ => exceptionStatus.ToString()
};
<li style="margin: 0; padding: 0; list-style: none;">
<div class="form-check" style="margin: 0; padding: 0;">
<input class="form-check-input d-none"
type="checkbox"
id="status-@exceptionStatus"
@onchange="() => ToggleExceptionStatus(exceptionStatus)" />
<label class="form-check-label w-100 d-block @(isSelected ? "bg-light" : "")"
for="status-@exceptionStatus"
style="cursor: pointer; margin: 0; padding: 12px 16px; display: block;">
@exceptionStatus
</label>
</div>
</li>
}
</ul>
</div>

<div class="dropdown">
<button class="btn btn-outline-success rounded-pill" type="button" data-bs-toggle="dropdown">
@GetSelectedTimeText()
</button>
<ul class="dropdown-menu" style="min-width: 200px; padding: 0; margin: 0;">
@foreach (var period in _timePeriods)
{
bool isSelected = _selectedTime == period.Key;
<li style="margin: 0; padding: 0; list-style: none;">
<div class="form-check" style="margin: 0; padding: 0;">
<input class="form-check-input d-none"
type="radio"
name="timePeriod"
id="time-@period.Key"
@onchange="() => SelectTimePeriod(period.Key)" />
<label class="form-check-label w-100 d-block @(isSelected ? "bg-light" : "")"
for="time-@period.Key"
style="cursor: pointer; margin: 0; padding: 12px 16px; display: block;">
@period.Value
</label>
</div>
</li>
}
</ul>
</div>

@if (_showCustomDateModal)
{
<div class="modal fade show" style="display: block; background-color: rgba(0,0,0,0.5);" tabindex="-1">
<div class="modal-dialog">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title">Select Date Range</h5>
<button type="button" class="btn-close" @onclick="CloseCustomDateModal"></button>
</div>
<div class="modal-body">
<div class="mb-3">
<label class="form-label">Start Date</label>
<InputDate class="form-control" @bind-value="_customStartDate" />
</div>
<div class="mb-3">
<label class="form-label">End Date</label>
<InputDate class="form-control" @bind-value="_customEndDate" />
</div>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-secondary" @onclick="CloseCustomDateModal">Cancel</button>
<button type="button" class="btn btn-primary" @onclick="ApplyCustomDateFilter">Filter</button>
</div>
</div>
</div>
</div>
}
</div>
</div>

@foreach (var i in FilteredInvoices)
{
<InvoiceItem Invoice="@i" class="box"/>
}
Expand All @@ -35,6 +156,14 @@
</div>

@code {
private DateTimeOffset? _customStartDate;
private DateTimeOffset? _customEndDate;
private bool _showCustomDateModal;
private string? _searchText;
private string _selectedTime = "all";
private HashSet<InvoiceStatus> _selectedStatuses = new();
private HashSet<InvoiceExceptionStatus> _selectedExceptionStatuses = new();

[Parameter]
public IEnumerable<InvoiceData>? Invoices { get; set; }

Expand All @@ -51,6 +180,97 @@

private string? StoreId => AccountManager.CurrentStore?.Id;

private IEnumerable<InvoiceData> FilteredInvoices => Invoices?
.Where(i => string.IsNullOrEmpty(_searchText) || i.Id.Contains(_searchText, StringComparison.OrdinalIgnoreCase))
.Where(i => _selectedStatuses.Count == 0 || _selectedStatuses.Contains(i.Status))
.Where(i => _selectedExceptionStatuses.Count == 0 || _selectedExceptionStatuses.Contains(i.AdditionalStatus))
.Where(i => FilterByTime(i))
?? Enumerable.Empty<InvoiceData>();


private string GetSelectedStatusText()
{
var count = _selectedStatuses.Count + _selectedExceptionStatuses.Count;
return count switch
{
0 => "All Status",
_ => $"{count} Status"
};
}

private void ToggleExceptionStatus(InvoiceExceptionStatus status)
{
bool wasPresent = _selectedExceptionStatuses.Remove(status);
if (!wasPresent)
_selectedExceptionStatuses.Add(status);

StateHasChanged();
}

private void ToggleStatus(InvoiceStatus status)
{
bool wasPresent = _selectedStatuses.Remove(status);
if (!wasPresent)
_selectedStatuses.Add(status);

StateHasChanged();
}

private void SelectTimePeriod(string period)
{
if (period == "custom")
{
_showCustomDateModal = true;
}
else
{
_selectedTime = period;
StateHasChanged();
}
}

private Dictionary<string, string> _timePeriods = new()
{
["24h"] = "24 Hours",
["3d"] = "3 Days",
["7d"] = "7 Days",
["custom"] = "Custom Range",
["all"] = "All Time"
};

private string GetSelectedTimeText()
{
return _timePeriods.TryGetValue(_selectedTime, out var text) ? text : "All Time";
}

private void CloseCustomDateModal()
{
_showCustomDateModal = false;
StateHasChanged();
}

private void ApplyCustomDateFilter()
{
_selectedTime = "custom";
CloseCustomDateModal();
}

private bool FilterByTime(InvoiceData invoice)
{
DateTimeOffset now = DateTimeOffset.Now;
DateTimeOffset created = invoice.CreatedTime;
TimeSpan timeSinceCreation = now - created;
return _selectedTime switch
{
"24h" => timeSinceCreation <= TimeSpan.FromHours(24),
"3d" => timeSinceCreation <= TimeSpan.FromDays(3),
"7d" => timeSinceCreation <= TimeSpan.FromDays(7),
"custom" => (_customStartDate == null || created >= _customStartDate) &&
(_customEndDate == null || created <= _customEndDate),
_ => true
};
}

protected override async Task OnAfterRenderAsync(bool firstRender)
{
if (firstRender)
Expand All @@ -65,4 +285,9 @@
{
await InvokeAsync(StateHasChanged);
}

private void OnFilterChanged(ChangeEventArgs e)
{
StateHasChanged();
}
}
Loading