Add recent receiver suggestions to wizard

Introduced a feature to display and apply recent receiver
suggestions in the `CreateEnvelopeWizardPopup` component.
Added a new section in step 2 to show recently used receivers,
with loading and state management logic. Updated the
`ReceiverService` to fetch suggestions from the backend API.

Enhanced the UI with new styles for the suggestions section
and improved localization support. Refactored state
management to handle loading, resetting, and applying
suggestions. Minor cleanup and adjustments to ensure proper
state transitions.
This commit is contained in:
2026-10-05 16:53:57 +02:00
parent 5627483589
commit a3b11ed63f
3 changed files with 138 additions and 0 deletions

View File

@@ -1,10 +1,13 @@
@using EnvelopeGenerator.Application.Common.Dto
@using EnvelopeGenerator.Application.Common.Dto.Receiver
@using EnvelopeGenerator.Application.Resources
@using EnvelopeGenerator.Domain.Resources
@using EnvelopeGenerator.Server.Client.Services
@using Microsoft.AspNetCore.Components.Forms
@using Microsoft.Extensions.Localization
@inject IStringLocalizer<Model> ModelLocalizer
@inject IStringLocalizer<Resource> UiLocalizer
@inject ReceiverService ReceiverService
<DxPopup Visible="@_isVisible"
VisibleChanged="OnPopupVisibleChangedAsync"
@@ -102,6 +105,30 @@
}
else if (_currentStep == 2)
{
<div class="sender-create-wizard__suggestions">
<div class="sender-create-wizard__suggestions-title">@L("SenderWizard_RecentReceivers", "Zuletzt verwendete Empfänger")</div>
@if (_isLoadingRecentSuggestions)
{
<div class="sender-create-wizard__empty-list">@L("Loading", "Lädt...")</div>
}
else if (_recentReceiverSuggestions.Count > 0)
{
<div class="sender-create-wizard__suggestion-list">
@foreach (var suggestion in _recentReceiverSuggestions)
{
<button type="button"
class="sender-create-wizard__suggestion-item"
@onclick="() => ApplyRecentSuggestion(suggestion)"
disabled="@IsActionLocked">
<span class="sender-create-wizard__suggestion-name">@suggestion.FullName</span>
<span class="sender-create-wizard__suggestion-email">@suggestion.EmailAddress</span>
</button>
}
</div>
}
</div>
<div class="sender-create-wizard__form-grid">
<div class="sender-create-wizard__field">
<label class="sender-create-wizard__label">
@@ -261,6 +288,9 @@
private string _receiverName = string.Empty;
private string _receiverEmail = string.Empty;
private readonly List<CreateEnvelopeWizardReceiver> _receivers = [];
private readonly List<RecentReceiverSuggestionDto> _recentReceiverSuggestions = [];
private bool _isLoadingRecentSuggestions;
private bool _recentSuggestionsLoaded;
private byte[]? _documentBytes;
private string? _documentFileName;
@@ -458,7 +488,12 @@
_validationMessage = null;
if (!IsLastStep)
{
_currentStep++;
if (_currentStep == 2)
await EnsureRecentSuggestionsLoadedAsync();
}
}
finally
{
@@ -569,6 +604,41 @@
await VisibleChanged.InvokeAsync(value);
}
private async Task EnsureRecentSuggestionsLoadedAsync()
{
if (_recentSuggestionsLoaded || _isLoadingRecentSuggestions)
return;
_isLoadingRecentSuggestions = true;
try
{
var suggestions = await ReceiverService.GetRecentReceiverSuggestionsAsync();
_recentReceiverSuggestions.Clear();
_recentReceiverSuggestions.AddRange(suggestions);
_recentSuggestionsLoaded = true;
}
catch
{
_recentReceiverSuggestions.Clear();
_recentSuggestionsLoaded = true;
}
finally
{
_isLoadingRecentSuggestions = false;
await InvokeAsync(StateHasChanged);
}
}
private void ApplyRecentSuggestion(RecentReceiverSuggestionDto suggestion)
{
if (IsActionLocked)
return;
_receiverName = suggestion.FullName;
_receiverEmail = suggestion.EmailAddress;
_validationMessage = null;
}
private bool ValidateCurrentStep()
{
if (_currentStep == 0)
@@ -640,6 +710,9 @@
_receiverName = string.Empty;
_receiverEmail = string.Empty;
_receivers.Clear();
_recentReceiverSuggestions.Clear();
_isLoadingRecentSuggestions = false;
_recentSuggestionsLoaded = false;
_documentBytes = null;
_documentFileName = null;

View File

@@ -1,5 +1,6 @@
using System.Net.Http.Json;
using System.Text.Json;
using EnvelopeGenerator.Application.Common.Dto.Receiver;
namespace EnvelopeGenerator.Server.Client.Services;
@@ -26,4 +27,21 @@ public class ReceiverService(IHttpClientFactory httpClientFactory)
var emails = await response.Content.ReadFromJsonAsync<List<string>>(JsonOptions, cancel);
return emails ?? [];
}
/// <summary>
/// Fetches recent receiver suggestions for the authenticated sender.
/// Calls GET /api/EnvelopeReceiver/recent-suggestions?take={take}.
/// </summary>
public async Task<IReadOnlyList<RecentReceiverSuggestionDto>> GetRecentReceiverSuggestionsAsync(int take = 5, CancellationToken cancel = default)
{
using var http = httpClientFactory.CreateClient("EnvelopeGenerator.Server");
var url = $"/api/EnvelopeReceiver/recent-suggestions?take={take}";
var response = await http.GetAsync(url, cancel);
if (!response.IsSuccessStatusCode)
return [];
var suggestions = await response.Content.ReadFromJsonAsync<List<RecentReceiverSuggestionDto>>(JsonOptions, cancel);
return suggestions ?? [];
}
}

View File

@@ -1271,6 +1271,53 @@
justify-content: flex-end;
}
.sender-create-wizard__suggestions {
display: flex;
flex-direction: column;
gap: 0.45rem;
}
.sender-create-wizard__suggestions-title {
font-size: 0.8rem;
font-weight: 700;
color: #475569;
}
.sender-create-wizard__suggestion-list {
display: flex;
flex-wrap: wrap;
gap: 0.45rem;
}
.sender-create-wizard__suggestion-item {
display: inline-flex;
flex-direction: column;
align-items: flex-start;
gap: 0.12rem;
border: 1px solid rgba(14, 116, 144, 0.22);
border-radius: 999px;
background: rgba(236, 254, 255, 0.85);
color: #0f172a;
padding: 0.35rem 0.7rem;
min-width: 0;
}
.sender-create-wizard__suggestion-item:disabled {
opacity: 0.58;
}
.sender-create-wizard__suggestion-name {
font-size: 0.77rem;
font-weight: 700;
line-height: 1.1;
}
.sender-create-wizard__suggestion-email {
font-size: 0.72rem;
color: #155e75;
line-height: 1.1;
}
.sender-create-wizard__receiver-list {
display: flex;
flex-direction: column;