Add advanced envelope settings and EnvelopeTypeService

Enhanced `EnvelopeDto` with new properties for advanced settings, including final email options, reminders, and expiration settings. Updated `SenderEditorPage.razor` to include a new UI section for configuring these settings, with dropdowns, checkboxes, and input fields.

Introduced `EnvelopeTypeService` to fetch envelope type data from the backend and applied defaults based on the selected envelope type. Registered the service in `Program.cs` for dependency injection.

Improved lifecycle methods in `SenderEditorPage.razor` to load and apply envelope settings. Enhanced localization, error handling, and data binding for the new settings. Updated `CreateEnvelopeCommand` to include the new properties.

Refactored code for better organization and maintainability, and added logging for debugging envelope type loading issues.
This commit is contained in:
2026-10-01 16:17:34 +02:00
parent af674a811d
commit 61d04ca51a
5 changed files with 300 additions and 2 deletions

View File

@@ -86,6 +86,41 @@ public record EnvelopeDto : IEnvelope
/// </summary> /// </summary>
public int? CertificationType { get; set; } public int? CertificationType { get; set; }
/// <summary>
///
/// </summary>
public int? FinalEmailToCreator { get; set; }
/// <summary>
///
/// </summary>
public int? FinalEmailToReceivers { get; set; }
/// <summary>
///
/// </summary>
public bool? SendReminderEmails { get; set; }
/// <summary>
///
/// </summary>
public int? FirstReminderDays { get; set; }
/// <summary>
///
/// </summary>
public int? ReminderIntervalDays { get; set; }
/// <summary>
///
/// </summary>
public int? ExpiresWhenDays { get; set; }
/// <summary>
///
/// </summary>
public int? ExpiresWarningWhenDays { get; set; }
/// <summary> /// <summary>
/// ///
/// </summary> /// </summary>
@@ -135,4 +170,4 @@ public record EnvelopeDto : IEnvelope
/// Envelope history entries tracking actions like DocumentSigned, EnvelopeOpened, etc. /// Envelope history entries tracking actions like DocumentSigned, EnvelopeOpened, etc.
/// </summary> /// </summary>
public IEnumerable<HistoryDto>? Histories { get; set; } public IEnumerable<HistoryDto>? Histories { get; set; }
} }

View File

@@ -5,6 +5,7 @@
@using EnvelopeGenerator.Application.Common.Dto @using EnvelopeGenerator.Application.Common.Dto
@using EnvelopeGenerator.Application.Envelopes.Commands @using EnvelopeGenerator.Application.Envelopes.Commands
@using EnvelopeGenerator.Application.Resources @using EnvelopeGenerator.Application.Resources
@using EnvelopeGenerator.Domain.Constants
@using EnvelopeGenerator.Server.Client.Components.PdfEditor @using EnvelopeGenerator.Server.Client.Components.PdfEditor
@using EnvelopeGenerator.Server.Client.Services @using EnvelopeGenerator.Server.Client.Services
@using Microsoft.AspNetCore.WebUtilities @using Microsoft.AspNetCore.WebUtilities
@@ -18,6 +19,7 @@
@inject PdfRenderService PdfRenderService @inject PdfRenderService PdfRenderService
@inject ReceiverService ReceiverService @inject ReceiverService ReceiverService
@inject EnvelopeService EnvelopeService @inject EnvelopeService EnvelopeService
@inject EnvelopeTypeService EnvelopeTypeService
@inject IStringLocalizer<Resource> UiLocalizer @inject IStringLocalizer<Resource> UiLocalizer
<link href="_content/DevExpress.Blazor.Themes/blazing-berry.bs5.min.css" rel="stylesheet" /> <link href="_content/DevExpress.Blazor.Themes/blazing-berry.bs5.min.css" rel="stylesheet" />
@@ -94,6 +96,107 @@
} }
</div> </div>
<details style="border:1px solid #dbe5ef;border-radius:8px;background:#f9fbfd;">
<summary style="cursor:pointer;list-style:none;display:flex;align-items:center;justify-content:space-between;padding:0.55rem 0.7rem;font-size:0.74rem;font-weight:700;color:#1f2937;text-transform:uppercase;letter-spacing:0.04em;">
<span>@L("SenderEditor_EnvelopeSettings", "Envelope-Einstellungen")</span>
<span style="font-size:0.66rem;color:#64748b;font-weight:600;">@L("SenderEditor_AdvancedOptional", "Erweitert")</span>
</summary>
<div style="padding:0 0.7rem 0.7rem;display:grid;grid-template-columns:repeat(auto-fit,minmax(180px,1fr));gap:0.45rem;">
<div style="display:flex;flex-direction:column;gap:0.2rem;">
<label style="font-size:0.65rem;font-weight:600;color:#64748b;">@L("EnvelopeType", "Umschlagtyp")</label>
<select value="@(_selectedEnvelopeTypeId?.ToString() ?? string.Empty)"
@onchange="OnEnvelopeTypeChanged"
style="font-size:0.78rem;border:1px solid #d1d5db;border-radius:5px;padding:0.3rem 0.45rem;background:#fff;">
<option value="">@L("SenderEditor_SelectTemplate", "Vorlage auswählen")</option>
@foreach (var type in _envelopeTypes)
{
<option value="@type.Id">@type.Title</option>
}
</select>
</div>
<div style="display:flex;flex-direction:column;gap:0.2rem;">
<label style="font-size:0.65rem;font-weight:600;color:#64748b;">@L("Language", "Sprache")</label>
<select @bind="_language"
style="font-size:0.78rem;border:1px solid #d1d5db;border-radius:5px;padding:0.3rem 0.45rem;background:#fff;">
<option value="de">Deutsch (de)</option>
<option value="en">English (en)</option>
<option value="de-DE">Deutsch (de-DE)</option>
<option value="en-US">English (en-US)</option>
</select>
</div>
<div style="display:flex;flex-direction:column;gap:0.2rem;">
<label style="font-size:0.65rem;font-weight:600;color:#64748b;">@L("CertificationType", "Zertifizierung")</label>
<select @bind="_certificationType"
style="font-size:0.78rem;border:1px solid #d1d5db;border-radius:5px;padding:0.3rem 0.45rem;background:#fff;">
<option value="1">@L("AdvancedElectronicSignature", "Advanced Electronic Signature")</option>
<option value="2">@L("ElectronicSignature", "Electronic Signature")</option>
<option value="3">@L("QualifiedSignature", "Qualified Signature")</option>
</select>
</div>
<div style="display:flex;flex-direction:column;gap:0.3rem;justify-content:flex-end;padding-bottom:0.25rem;">
<label style="display:flex;align-items:center;gap:0.45rem;font-size:0.74rem;color:#1f2937;">
<input type="checkbox" checked="@_tfaEnabled" @onchange="OnTfaChanged" />
@L("TFAEnabled", "2FA aktiviert")
</label>
<label style="display:flex;align-items:center;gap:0.45rem;font-size:0.74rem;color:#1f2937;">
<input type="checkbox" checked="@_useAccessCode" @onchange="OnUseAccessCodeChanged" disabled="@_tfaEnabled" />
@L("UseAccessCode", "Zugriffscode verwenden")
</label>
</div>
<div style="display:flex;flex-direction:column;gap:0.2rem;">
<label style="font-size:0.65rem;font-weight:600;color:#64748b;">@L("FinalEmailToCreator", "Finale E-Mail an Ersteller")</label>
<select @bind="_finalEmailToCreator"
style="font-size:0.78rem;border:1px solid #d1d5db;border-radius:5px;padding:0.3rem 0.45rem;background:#fff;">
<option value="0">@L("No", "Nein")</option>
<option value="1">@L("Yes", "Ja")</option>
<option value="2">@L("YesWithAttachment", "Ja, mit Anhang")</option>
</select>
</div>
<div style="display:flex;flex-direction:column;gap:0.2rem;">
<label style="font-size:0.65rem;font-weight:600;color:#64748b;">@L("FinalEmailToReceivers", "Finale E-Mail an Empfänger")</label>
<select @bind="_finalEmailToReceivers"
style="font-size:0.78rem;border:1px solid #d1d5db;border-radius:5px;padding:0.3rem 0.45rem;background:#fff;">
<option value="0">@L("No", "Nein")</option>
<option value="1">@L("Yes", "Ja")</option>
<option value="2">@L("YesWithAttachment", "Ja, mit Anhang")</option>
</select>
</div>
<div style="display:flex;flex-direction:column;gap:0.3rem;justify-content:flex-end;padding-bottom:0.25rem;">
<label style="display:flex;align-items:center;gap:0.45rem;font-size:0.74rem;color:#1f2937;">
<input type="checkbox" checked="@_sendReminderEmails" @onchange="OnSendReminderEmailsChanged" />
@L("SendReminderEmails", "Erinnerungs-E-Mails senden")
</label>
</div>
<div style="display:flex;flex-direction:column;gap:0.2rem;">
<label style="font-size:0.65rem;font-weight:600;color:#64748b;">@L("FirstReminderDays", "Erste Erinnerung (Tage)")</label>
<InputNumber @bind-Value="_firstReminderDays" class="form-control form-control-sm" disabled="@(!_sendReminderEmails)" />
</div>
<div style="display:flex;flex-direction:column;gap:0.2rem;">
<label style="font-size:0.65rem;font-weight:600;color:#64748b;">@L("ReminderIntervalDays", "Erinnerungsintervall (Tage)")</label>
<InputNumber @bind-Value="_reminderIntervalDays" class="form-control form-control-sm" disabled="@(!_sendReminderEmails)" />
</div>
<div style="display:flex;flex-direction:column;gap:0.2rem;">
<label style="font-size:0.65rem;font-weight:600;color:#64748b;">@L("ExpiresWhenDays", "Läuft ab nach (Tage)")</label>
<InputNumber @bind-Value="_expiresWhenDays" class="form-control form-control-sm" />
</div>
<div style="display:flex;flex-direction:column;gap:0.2rem;">
<label style="font-size:0.65rem;font-weight:600;color:#64748b;">@L("ExpiresWarningWhenDays", "Ablaufwarnung (Tage)")</label>
<InputNumber @bind-Value="_expiresWarningWhenDays" class="form-control form-control-sm" />
</div>
</div>
</details>
<div class="sender-receivers-panel"> <div class="sender-receivers-panel">
<div class="sender-receivers-panel__header"> <div class="sender-receivers-panel__header">
<div class="sender-receivers-panel__title-wrap"> <div class="sender-receivers-panel__title-wrap">
@@ -560,6 +663,20 @@
string _envelopeMessage = string.Empty; string _envelopeMessage = string.Empty;
bool _titleTouched = false; bool _titleTouched = false;
List<EnvelopeTypeDto> _envelopeTypes = [];
int? _selectedEnvelopeTypeId;
string _language = "de-DE";
bool _useAccessCode;
bool _tfaEnabled;
int _certificationType = (int)CertificationType.AdvancedElectronicSignature;
int _finalEmailToCreator = (int)FinalEmailType.No;
int _finalEmailToReceivers = (int)FinalEmailType.No;
bool _sendReminderEmails;
int _firstReminderDays;
int _reminderIntervalDays;
int _expiresWhenDays;
int _expiresWarningWhenDays;
List<ReceiverDraft> _receivers = []; List<ReceiverDraft> _receivers = [];
bool _receiverPopupVisible; bool _receiverPopupVisible;
string _receiverDraftName = string.Empty; string _receiverDraftName = string.Empty;
@@ -577,6 +694,25 @@
// ── Lifecycle ── // ── Lifecycle ──
protected override async Task OnInitializedAsync()
{
try
{
_envelopeTypes = (await EnvelopeTypeService.GetAsync()).ToList();
if (!IsEditMode && _selectedEnvelopeTypeId is null && _envelopeTypes.Count > 0)
{
_selectedEnvelopeTypeId = _envelopeTypes[0].Id;
ApplyEnvelopeTypeDefaults(_envelopeTypes[0]);
}
}
catch (Exception ex)
{
Logger.LogWarning(ex, "Failed to load envelope types for sender editor.");
_envelopeTypes = [];
}
}
protected override async Task OnParametersSetAsync() protected override async Task OnParametersSetAsync()
{ {
if (EnvelopeId.HasValue && EnvelopeId.Value > 0) if (EnvelopeId.HasValue && EnvelopeId.Value > 0)
@@ -621,6 +757,22 @@
_envelopeTitle = envelope.Title ?? string.Empty; _envelopeTitle = envelope.Title ?? string.Empty;
_envelopeMessage = envelope.Message ?? string.Empty; _envelopeMessage = envelope.Message ?? string.Empty;
_selectedEnvelopeTypeId = envelope.EnvelopeTypeId;
_language = string.IsNullOrWhiteSpace(envelope.Language) ? "de-DE" : envelope.Language;
_useAccessCode = envelope.UseAccessCode;
_tfaEnabled = envelope.TFAEnabled;
if (_tfaEnabled)
_useAccessCode = true;
_certificationType = envelope.CertificationType ?? (int)CertificationType.AdvancedElectronicSignature;
_finalEmailToCreator = envelope.FinalEmailToCreator ?? (int)FinalEmailType.No;
_finalEmailToReceivers = envelope.FinalEmailToReceivers ?? (int)FinalEmailType.No;
_sendReminderEmails = envelope.EnvelopeType?.SendReminderEmails ?? false;
_firstReminderDays = envelope.EnvelopeType?.FirstReminderDays ?? 0;
_reminderIntervalDays = envelope.EnvelopeType?.ReminderIntervalDays ?? 0;
_expiresWhenDays = envelope.ExpiresWhenDays ?? envelope.EnvelopeType?.ExpiresDays ?? 0;
_expiresWarningWhenDays = envelope.ExpiresWarningWhenDays ?? envelope.EnvelopeType?.ExpiresWarningDays ?? 0;
var envelopeReceivers = envelope.EnvelopeReceivers?.ToList() ?? []; var envelopeReceivers = envelope.EnvelopeReceivers?.ToList() ?? [];
_receivers = envelopeReceivers _receivers = envelopeReceivers
@@ -755,6 +907,82 @@
void CancelPlacement() => _pendingReceiverForPlacement = null; void CancelPlacement() => _pendingReceiverForPlacement = null;
Task OnEnvelopeTypeChanged(ChangeEventArgs e)
{
var raw = e.Value?.ToString();
if (!int.TryParse(raw, out var id) || id <= 0)
{
_selectedEnvelopeTypeId = null;
return Task.CompletedTask;
}
_selectedEnvelopeTypeId = id;
var selectedType = _envelopeTypes.FirstOrDefault(x => x.Id == id);
if (selectedType is not null)
ApplyEnvelopeTypeDefaults(selectedType);
return Task.CompletedTask;
}
void ApplyEnvelopeTypeDefaults(EnvelopeTypeDto envelopeType)
{
if (!string.IsNullOrWhiteSpace(envelopeType.Language))
_language = envelopeType.Language;
if (envelopeType.UseAccessCode.HasValue)
_useAccessCode = envelopeType.UseAccessCode.Value;
if (_tfaEnabled)
_useAccessCode = true;
if (envelopeType.CertificationType.HasValue)
_certificationType = envelopeType.CertificationType.Value;
if (envelopeType.FinalEmailToCreator.HasValue)
_finalEmailToCreator = envelopeType.FinalEmailToCreator.Value;
if (envelopeType.FinalEmailToReceivers.HasValue)
_finalEmailToReceivers = envelopeType.FinalEmailToReceivers.Value;
if (envelopeType.SendReminderEmails.HasValue)
_sendReminderEmails = envelopeType.SendReminderEmails.Value;
if (envelopeType.FirstReminderDays.HasValue)
_firstReminderDays = envelopeType.FirstReminderDays.Value;
if (envelopeType.ReminderIntervalDays.HasValue)
_reminderIntervalDays = envelopeType.ReminderIntervalDays.Value;
if (envelopeType.ExpiresDays.HasValue)
_expiresWhenDays = envelopeType.ExpiresDays.Value;
if (envelopeType.ExpiresWarningDays.HasValue)
_expiresWarningWhenDays = envelopeType.ExpiresWarningDays.Value;
}
void OnTfaChanged(ChangeEventArgs e)
{
_tfaEnabled = e.Value as bool? ?? false;
if (_tfaEnabled)
_useAccessCode = true;
}
void OnUseAccessCodeChanged(ChangeEventArgs e)
{
if (_tfaEnabled)
{
_useAccessCode = true;
return;
}
_useAccessCode = e.Value as bool? ?? false;
}
void OnSendReminderEmailsChanged(ChangeEventArgs e)
{
_sendReminderEmails = e.Value as bool? ?? false;
}
// ── PDF area click → place field ── // ── PDF area click → place field ──
async Task OnPdfAreaClickAsync(MouseEventArgs e) async Task OnPdfAreaClickAsync(MouseEventArgs e)
{ {
@@ -1031,9 +1259,20 @@
return new CreateEnvelopeCommand return new CreateEnvelopeCommand
{ {
EnvelopeId = envelopeId, EnvelopeId = envelopeId,
EnvelopeTypeId = _selectedEnvelopeTypeId,
Title = _envelopeTitle, Title = _envelopeTitle,
Message = _envelopeMessage, Message = _envelopeMessage,
TFAEnabled = false, Language = _language,
UseAccessCode = _useAccessCode,
TFAEnabled = _tfaEnabled,
CertificationType = _certificationType,
FinalEmailToCreator = _finalEmailToCreator,
FinalEmailToReceivers = _finalEmailToReceivers,
SendReminderEmails = _sendReminderEmails,
FirstReminderDays = _firstReminderDays,
ReminderIntervalDays = _reminderIntervalDays,
ExpiresWhenDays = _expiresWhenDays,
ExpiresWarningWhenDays = _expiresWarningWhenDays,
Send = send, Send = send,
Document = string.IsNullOrWhiteSpace(docBase64) ? null : new CreateEnvelopeDocumentDto(docBase64), Document = string.IsNullOrWhiteSpace(docBase64) ? null : new CreateEnvelopeDocumentDto(docBase64),
Receivers = receiverCommands Receivers = receiverCommands

View File

@@ -41,6 +41,7 @@ builder.Services.AddScoped<SignatureService>();
builder.Services.AddScoped<SignatureCacheService>(); builder.Services.AddScoped<SignatureCacheService>();
builder.Services.AddSingleton<AppVersionService>(); builder.Services.AddSingleton<AppVersionService>();
builder.Services.AddScoped<EnvelopeService>(); builder.Services.AddScoped<EnvelopeService>();
builder.Services.AddScoped<EnvelopeTypeService>();
builder.Services.AddScoped<LocalizationService>(); builder.Services.AddScoped<LocalizationService>();
builder.Services.AddScoped<ReceiverService>(); builder.Services.AddScoped<ReceiverService>();
builder.Services.AddScoped<PdfRenderService>(); builder.Services.AddScoped<PdfRenderService>();

View File

@@ -0,0 +1,22 @@
using EnvelopeGenerator.Application.Common.Dto;
using System.Net.Http.Json;
using System.Text.Json;
namespace EnvelopeGenerator.Server.Client.Services;
public class EnvelopeTypeService(IHttpClientFactory clientFactory)
{
private static readonly JsonSerializerOptions JsonOptions = new(JsonSerializerDefaults.Web);
public async Task<IReadOnlyList<EnvelopeTypeDto>> GetAsync(CancellationToken cancel = default)
{
var http = clientFactory.CreateClient("EnvelopeGenerator.Server");
var response = await http.GetAsync("/api/EnvelopeType", cancel);
if (!response.IsSuccessStatusCode)
return [];
var data = await response.Content.ReadFromJsonAsync<List<EnvelopeTypeDto>>(JsonOptions, cancel);
return data ?? [];
}
}

View File

@@ -345,6 +345,7 @@ try
builder.Services.AddScoped<EnvelopeGenerator.Server.Client.Services.LocalizationService>(); builder.Services.AddScoped<EnvelopeGenerator.Server.Client.Services.LocalizationService>();
builder.Services.AddScoped<EnvelopeGenerator.Server.Client.Services.ReceiverService>(); builder.Services.AddScoped<EnvelopeGenerator.Server.Client.Services.ReceiverService>();
builder.Services.AddScoped<EnvelopeGenerator.Server.Client.Services.PdfRenderService>(); builder.Services.AddScoped<EnvelopeGenerator.Server.Client.Services.PdfRenderService>();
builder.Services.AddScoped<EnvelopeGenerator.Server.Client.Services.EnvelopeTypeService>();
// Business Services (Server specific) // Business Services (Server specific)
builder.Services.AddScoped<DocumentService>(); builder.Services.AddScoped<DocumentService>();