Add receiver management to EnvelopeSenderEditorPage
Introduced a new "Receivers" panel in `EnvelopeSenderEditorPage` to manage receivers. Added a popup for adding receivers with validation, email suggestions, and caching for performance. Updated the layout to display receiver details and signature fields. Injected `EnvelopeReceiverPageDataService` for receiver-related operations. Added a `ReceiverDraft` model and implemented methods for managing receivers. Enhanced CSS for the new UI elements and ensured responsiveness. Minor refactoring and cleanup included.
This commit is contained in:
@@ -1,13 +1,15 @@
|
||||
@page "/envelope/editor"
|
||||
@page "/sender/editor"
|
||||
@rendermode InteractiveServer
|
||||
@using DevExpress.Blazor.PdfViewer
|
||||
@using DevExpress.Blazor.Reporting.Models
|
||||
@using DevExpress.Blazor
|
||||
@using EnvelopeGenerator.Server.Client.Services
|
||||
@using EnvelopeGenerator.Server.Services
|
||||
@using Microsoft.AspNetCore.Components.Forms
|
||||
@inject IJSRuntime JSRuntime
|
||||
@inject AppVersionService AppVersion
|
||||
@inject ILogger<EnvelopeSenderEditorPage> Logger
|
||||
@inject EnvelopeReceiverPageDataService ReceiverPageDataService
|
||||
|
||||
<link href="_content/DevExpress.Blazor.Themes/blazing-berry.bs5.min.css" rel="stylesheet" />
|
||||
<link href="@AppVersion.GetVersionedUrl("css/envelope-viewer.css")" rel="stylesheet" />
|
||||
@@ -21,22 +23,66 @@
|
||||
style="flex-direction: row; align-items: center; padding: 0.35rem 1.5rem; gap: 0.75rem;">
|
||||
|
||||
@* Left: Title *@
|
||||
<div style="flex: 1; min-width: 0; display: flex; align-items: center; gap: 0.75rem;">
|
||||
<div style="font-size: 0.9rem; font-weight: 600; color: #1f2937; white-space: nowrap;">
|
||||
Neues Dokument
|
||||
</div>
|
||||
@if (_pdfLoaded)
|
||||
{
|
||||
<span style="font-size: 0.7rem; color: #6b7280;">@_fileName</span>
|
||||
@if (_signatureFields.Count > 0)
|
||||
<div style="flex: 1; min-width: 0; display: flex; flex-direction: column; align-items: stretch; gap: 0.75rem;">
|
||||
<div style="display: flex; align-items: center; gap: 0.75rem; min-width: 0; flex-wrap: wrap;">
|
||||
<div style="font-size: 0.9rem; font-weight: 600; color: #1f2937; white-space: nowrap;">
|
||||
Neues Dokument
|
||||
</div>
|
||||
@if (_pdfLoaded)
|
||||
{
|
||||
<span style="display: inline-flex; align-items: center; padding: 0.125rem 0.4rem;
|
||||
background: #ede9fe; border-radius: 0.25rem; color: #6d28d9;
|
||||
font-weight: 500; font-size: 0.7rem; white-space: nowrap;">
|
||||
@_signatureFields.Count Signaturfeld@(_signatureFields.Count != 1 ? "er" : "")
|
||||
</span>
|
||||
<span style="font-size: 0.7rem; color: #6b7280;">@_fileName</span>
|
||||
@if (_signatureFields.Count > 0)
|
||||
{
|
||||
<span style="display: inline-flex; align-items: center; padding: 0.125rem 0.4rem;
|
||||
background: #ede9fe; border-radius: 0.25rem; color: #6d28d9;
|
||||
font-weight: 500; font-size: 0.7rem; white-space: nowrap;">
|
||||
@_signatureFields.Count Signaturfeld@(_signatureFields.Count != 1 ? "er" : "")
|
||||
</span>
|
||||
}
|
||||
}
|
||||
}
|
||||
</div>
|
||||
|
||||
<div class="sender-receivers-panel">
|
||||
<div class="sender-receivers-panel__header">
|
||||
<div class="sender-receivers-panel__title-wrap">
|
||||
<span class="sender-receivers-panel__title">Empfänger</span>
|
||||
<span class="sender-receivers-panel__count">@_receivers.Count</span>
|
||||
</div>
|
||||
|
||||
<DxButton CssClass="sender-receivers-panel__add-btn pdf-toolbar-like-btn pdf-toolbar-like-btn--add"
|
||||
Text="Empfänger hinzufügen"
|
||||
RenderStyle="ButtonRenderStyle.Secondary"
|
||||
SizeMode="SizeMode.Small"
|
||||
Click="OpenAddReceiverPopup" />
|
||||
</div>
|
||||
|
||||
@if (_receivers.Count == 0)
|
||||
{
|
||||
<div class="sender-receivers-panel__empty">
|
||||
Noch keine Empfänger hinzugefügt.
|
||||
</div>
|
||||
}
|
||||
else
|
||||
{
|
||||
<div class="sender-receivers-list">
|
||||
@foreach (var receiver in _receivers)
|
||||
{
|
||||
<div class="sender-receiver-chip">
|
||||
<div class="sender-receiver-chip__body">
|
||||
<div class="sender-receiver-chip__name">@receiver.FullName</div>
|
||||
<div class="sender-receiver-chip__email">@receiver.Email</div>
|
||||
</div>
|
||||
|
||||
<DxButton CssClass="sender-receiver-chip__action pdf-toolbar-like-btn pdf-toolbar-like-btn--signature"
|
||||
Text="Signatur hinzufügen"
|
||||
RenderStyle="ButtonRenderStyle.Secondary"
|
||||
SizeMode="SizeMode.Small"
|
||||
Click="@(() => AddSignatureForReceiver(receiver))" />
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@* Right: Buttons *@
|
||||
@@ -208,6 +254,78 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<DxPopup @bind-Visible="_receiverPopupVisible"
|
||||
HeaderText="Empfänger hinzufügen"
|
||||
ShowFooter="true"
|
||||
CloseOnEscape="true"
|
||||
Width="720px"
|
||||
CssClass="sender-receiver-popup">
|
||||
<BodyContentTemplate Context="popupContext">
|
||||
<div class="sender-receiver-popup__body">
|
||||
<div class="sender-receiver-popup__form-grid">
|
||||
<div class="sender-receiver-popup__field">
|
||||
<div class="sender-receiver-popup__label">Vor- und Nachname</div>
|
||||
<DxTextBox Text="@_receiverDraftName"
|
||||
TextChanged="OnReceiverNameChanged"
|
||||
NullText="Max Mustermann"
|
||||
ClearButtonDisplayMode="DataEditorClearButtonDisplayMode.Auto"
|
||||
BindValueMode="BindValueMode.OnInput"
|
||||
CssClass="w-100" />
|
||||
</div>
|
||||
|
||||
<div class="sender-receiver-popup__field">
|
||||
<div class="sender-receiver-popup__label">E-Mail-Adresse</div>
|
||||
<DxTextBox Text="@_receiverDraftEmail"
|
||||
TextChanged="OnReceiverEmailTextChangedAsync"
|
||||
NullText="name@beispiel.de"
|
||||
ClearButtonDisplayMode="DataEditorClearButtonDisplayMode.Auto"
|
||||
BindValueMode="BindValueMode.OnInput"
|
||||
CssClass="w-100" />
|
||||
|
||||
<div class="sender-receiver-popup__suggestions-shell">
|
||||
@if (_receiverEmailSuggestions.Count > 0)
|
||||
{
|
||||
<div class="sender-receiver-popup__suggestions">
|
||||
<DxListBox TData="string"
|
||||
TValue="string"
|
||||
Data="@_receiverEmailSuggestions"
|
||||
Value="@_receiverDraftEmail"
|
||||
ValueChanged="OnReceiverEmailSuggestionSelectedAsync"
|
||||
SelectionMode="ListBoxSelectionMode.Single"
|
||||
CssClass="w-100" />
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="sender-receiver-popup__hint">
|
||||
Bereits verwendete E-Mail-Adressen werden bei der Eingabe vorgeschlagen.
|
||||
</div>
|
||||
|
||||
@if (_isReceiverEmailSearchRunning)
|
||||
{
|
||||
<div class="sender-receiver-popup__loading">E-Mail-Vorschläge werden geladen…</div>
|
||||
}
|
||||
|
||||
@if (!string.IsNullOrWhiteSpace(_receiverPopupValidationMessage))
|
||||
{
|
||||
<div class="sender-receiver-popup__validation">@_receiverPopupValidationMessage</div>
|
||||
}
|
||||
</div>
|
||||
</BodyContentTemplate>
|
||||
<FooterContentTemplate>
|
||||
<div class="sender-receiver-popup__footer">
|
||||
<DxButton Text="Abbrechen"
|
||||
RenderStyle="ButtonRenderStyle.Secondary"
|
||||
Click="CloseAddReceiverPopup" />
|
||||
<DxButton Text="Empfänger hinzufügen"
|
||||
RenderStyle="ButtonRenderStyle.Primary"
|
||||
Click="SaveReceiverAsync" />
|
||||
</div>
|
||||
</FooterContentTemplate>
|
||||
</DxPopup>
|
||||
|
||||
@code {
|
||||
// ── Constants ──
|
||||
// Signature field size in PDF points (fixed): 1.77" × 1.96" × 72 pt/inch
|
||||
@@ -227,6 +345,15 @@
|
||||
string? _errorMessage;
|
||||
bool _placementMode = false;
|
||||
List<SignatureFieldDraft> _signatureFields = [];
|
||||
List<ReceiverDraft> _receivers = [];
|
||||
bool _receiverPopupVisible;
|
||||
string _receiverDraftName = string.Empty;
|
||||
string _receiverDraftEmail = string.Empty;
|
||||
string? _receiverPopupValidationMessage;
|
||||
bool _isReceiverEmailSearchRunning;
|
||||
List<string> _receiverEmailSuggestions = [];
|
||||
int _receiverEmailSearchVersion;
|
||||
static readonly System.ComponentModel.DataAnnotations.EmailAddressAttribute ReceiverEmailValidator = new();
|
||||
|
||||
// ── PDF upload ──
|
||||
async Task OnPdfFileSelectedAsync(InputFileChangeEventArgs e)
|
||||
@@ -336,6 +463,120 @@
|
||||
$"[SenderEditor] Total fields: {_signatureFields.Count}");
|
||||
}
|
||||
|
||||
void OpenAddReceiverPopup()
|
||||
{
|
||||
_receiverDraftName = string.Empty;
|
||||
_receiverDraftEmail = string.Empty;
|
||||
_receiverPopupValidationMessage = null;
|
||||
_receiverEmailSuggestions.Clear();
|
||||
_receiverPopupVisible = true;
|
||||
}
|
||||
|
||||
void CloseAddReceiverPopup()
|
||||
{
|
||||
_receiverPopupVisible = false;
|
||||
_receiverPopupValidationMessage = null;
|
||||
_isReceiverEmailSearchRunning = false;
|
||||
}
|
||||
|
||||
void OnReceiverNameChanged(string? value)
|
||||
{
|
||||
_receiverDraftName = value ?? string.Empty;
|
||||
_receiverPopupValidationMessage = null;
|
||||
}
|
||||
|
||||
Task OnReceiverEmailSuggestionSelectedAsync(string? value)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(value))
|
||||
return Task.CompletedTask;
|
||||
|
||||
_receiverDraftEmail = value.Trim();
|
||||
_receiverPopupValidationMessage = null;
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
async Task OnReceiverEmailTextChangedAsync(string? value)
|
||||
{
|
||||
_receiverDraftEmail = value?.Trim() ?? string.Empty;
|
||||
_receiverPopupValidationMessage = null;
|
||||
|
||||
var searchVersion = ++_receiverEmailSearchVersion;
|
||||
|
||||
if (string.IsNullOrWhiteSpace(_receiverDraftEmail) || _receiverDraftEmail.Length < 2)
|
||||
{
|
||||
_receiverEmailSuggestions.Clear();
|
||||
_isReceiverEmailSearchRunning = false;
|
||||
return;
|
||||
}
|
||||
|
||||
_isReceiverEmailSearchRunning = true;
|
||||
|
||||
try
|
||||
{
|
||||
var results = await ReceiverPageDataService.SearchReceiverEMailsAsync(_receiverDraftEmail);
|
||||
|
||||
if (searchVersion != _receiverEmailSearchVersion)
|
||||
return;
|
||||
|
||||
_receiverEmailSuggestions = results
|
||||
.Where(email => !string.IsNullOrWhiteSpace(email))
|
||||
.Distinct(StringComparer.OrdinalIgnoreCase)
|
||||
.OrderBy(email => email)
|
||||
.Take(12)
|
||||
.ToList();
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Logger.LogWarning(ex, "Failed to load receiver email suggestions for {SearchTerm}", _receiverDraftEmail);
|
||||
if (searchVersion == _receiverEmailSearchVersion)
|
||||
_receiverEmailSuggestions.Clear();
|
||||
}
|
||||
finally
|
||||
{
|
||||
if (searchVersion == _receiverEmailSearchVersion)
|
||||
_isReceiverEmailSearchRunning = false;
|
||||
}
|
||||
}
|
||||
|
||||
Task SaveReceiverAsync()
|
||||
{
|
||||
var fullName = _receiverDraftName.Trim();
|
||||
var email = _receiverDraftEmail.Trim();
|
||||
|
||||
if (string.IsNullOrWhiteSpace(fullName))
|
||||
{
|
||||
_receiverPopupValidationMessage = "Bitte geben Sie einen Vor- und Nachnamen ein.";
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
if (string.IsNullOrWhiteSpace(email))
|
||||
{
|
||||
_receiverPopupValidationMessage = "Bitte geben Sie eine E-Mail-Adresse ein.";
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
if (!ReceiverEmailValidator.IsValid(email))
|
||||
{
|
||||
_receiverPopupValidationMessage = "Bitte geben Sie eine gültige E-Mail-Adresse ein.";
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
if (_receivers.Any(receiver => string.Equals(receiver.Email, email, StringComparison.OrdinalIgnoreCase)))
|
||||
{
|
||||
_receiverPopupValidationMessage = "Diese E-Mail-Adresse wurde bereits hinzugefügt.";
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
_receivers.Add(new ReceiverDraft(Guid.NewGuid(), fullName, email));
|
||||
CloseAddReceiverPopup();
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
void AddSignatureForReceiver(ReceiverDraft receiver)
|
||||
{
|
||||
Logger.LogInformation("Signature placement requested for receiver {Email}", receiver.Email);
|
||||
}
|
||||
|
||||
protected override async Task OnAfterRenderAsync(bool firstRender)
|
||||
{
|
||||
if (!_pdfLoaded || _errorMessage is not null)
|
||||
@@ -351,4 +592,6 @@
|
||||
record SignatureFieldDraft(double XPt, double YPt, int Page, double DisplayX, double DisplayY);
|
||||
|
||||
record OverlayCoords(double RelX, double RelY, double ContainerW, double ContainerH);
|
||||
|
||||
record ReceiverDraft(Guid Id, string FullName, string Email);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user