Introduced a "Send Confirmation" popup to improve user experience when sending envelopes. The popup allows users to review recipients, signature fields, and settings before confirming the send action. - Added localized strings in `Resource.de-DE.resx`, `Resource.en-US.resx`, and `Resource.fr-FR.resx` for confirmation messages and actions. - Updated `SenderEditorPage.razor` to include the confirmation popup (`DxPopup`) with body and footer templates. - Added `RequestSendAsync` and `ConfirmSendAsync` methods to manage the confirmation process and sending logic. - Replaced the old success popup logic with direct navigation to the sender dashboard after a successful send. - Updated the toolbar button to trigger the confirmation popup. - Styled the confirmation popup in `envelope-viewer.css`, including support for dark themes. - Ensured proper state management for the popup and sending process. These changes enhance usability by providing a clear confirmation step and progress feedback during envelope sending.
1655 lines
72 KiB
Plaintext
1655 lines
72 KiB
Plaintext
@page "/sender/editor"
|
|
@rendermode InteractiveWebAssembly
|
|
@using DevExpress.Blazor.Reporting.Models
|
|
@using DevExpress.Blazor
|
|
@using EnvelopeGenerator.Application.Common.Dto
|
|
@using EnvelopeGenerator.Application.Envelopes.Commands
|
|
@using EnvelopeGenerator.Application.Resources
|
|
@using EnvelopeGenerator.Domain.Constants
|
|
@using EnvelopeGenerator.Domain.Resources
|
|
@using EnvelopeGenerator.Application.Common.Configurations
|
|
@using EnvelopeGenerator.Server.Client.Components.PdfEditor
|
|
@using EnvelopeGenerator.Server.Client.Services
|
|
@using Microsoft.AspNetCore.Components.Forms
|
|
@using Microsoft.Extensions.Localization
|
|
@using Microsoft.Extensions.Options
|
|
@inject IJSRuntime JSRuntime
|
|
@inject NavigationManager NavigationManager
|
|
@inject AppVersionService AppVersion
|
|
@inject ILogger<SenderEditorPage> Logger
|
|
@inject DocumentService DocumentService
|
|
@inject PdfRenderService PdfRenderService
|
|
@inject ReceiverService ReceiverService
|
|
@inject EnvelopeReceiverService EnvelopeReceiverService
|
|
@inject EnvelopeService EnvelopeService
|
|
@inject EnvelopeTypeService EnvelopeTypeService
|
|
@inject AuthService AuthService
|
|
@inject IStringLocalizer<Resource> UiLocalizer
|
|
@inject IStringLocalizer<Model> ModelLocalizer
|
|
@inject IOptionsMonitor<SignatureFieldParams> SignatureFieldOptions
|
|
|
|
<link href="_content/DevExpress.Blazor.Themes/blazing-berry.bs5.min.css" rel="stylesheet" />
|
|
<link href="_content/DevExpress.Blazor.Viewer/css/dx-blazor-viewer-components.bs5.css" rel="stylesheet" />
|
|
<link href="@AppVersion.GetVersionedUrl("css/envelope-viewer.css")" rel="stylesheet" />
|
|
<script src="@AppVersion.GetVersionedUrl("js/envelope-editor.js")"></script>
|
|
|
|
<div class="envelope-viewer-layout">
|
|
|
|
@* ── Action Bar ── *@
|
|
<div class="envelope-action-bar">
|
|
<div class="envelope-action-bar__inner sender-editor-action-inner">
|
|
|
|
@* Left: Title + meta inputs + receivers *@
|
|
<div class="sender-editor-main-column">
|
|
|
|
@* ── Title & Message inputs ── *@
|
|
<div class="sender-editor-meta-row">
|
|
|
|
@* Titel *@
|
|
<div class="sender-editor-input-field sender-editor-input-field--title">
|
|
<label class="sender-editor-label">
|
|
@L("Title", "Titel") <span class="sender-editor-required-asterisk">*</span>
|
|
</label>
|
|
<input type="text"
|
|
value="@_envelopeTitle"
|
|
@oninput="e => _envelopeTitle = e.Value?.ToString() ?? string.Empty"
|
|
placeholder='@L("SenderEditor_Title_Placeholder", "z.B. Arbeitsvertrag 2025")'
|
|
maxlength="120"
|
|
class="sender-editor-text-input @(_titleTouched && string.IsNullOrWhiteSpace(_envelopeTitle) ? "sender-editor-text-input--invalid" : string.Empty)" />
|
|
@if (_titleTouched && string.IsNullOrWhiteSpace(_envelopeTitle))
|
|
{
|
|
<span class="sender-editor-required-text">@L("RequiredField", "Pflichtfeld")</span>
|
|
}
|
|
</div>
|
|
|
|
@* Nachricht *@
|
|
<div class="sender-editor-input-field sender-editor-input-field--message">
|
|
<label class="sender-editor-label">
|
|
@L("Message", "Nachricht") <span class="sender-editor-optional">(@L("Optional", "optional"))</span>
|
|
</label>
|
|
<input type="text"
|
|
value="@_envelopeMessage"
|
|
@oninput="e => _envelopeMessage = e.Value?.ToString() ?? string.Empty"
|
|
placeholder='@L("SenderEditor_Message_Placeholder", "Bitte unterzeichnen Sie das beigefügte Dokument.")'
|
|
maxlength="500"
|
|
class="sender-editor-text-input" />
|
|
</div>
|
|
|
|
@* PDF filename badge *@
|
|
@if (_pdfLoaded)
|
|
{
|
|
<div class="sender-editor-file-meta">
|
|
<span class="sender-editor-file-name">@_fileName</span>
|
|
@if (_signatureFields.Count > 0)
|
|
{
|
|
<span class="sender-editor-signature-count">
|
|
@_signatureFields.Count @L("SenderEditor_SignatureField", "Signaturfeld")@(_signatureFields.Count != 1 ? "er" : "")
|
|
</span>
|
|
}
|
|
</div>
|
|
}
|
|
</div>
|
|
|
|
<div class="sender-settings-shell">
|
|
<DxAccordion ExpandMode="AccordionExpandMode.SingleOrNone" CssClass="sender-settings-accordion">
|
|
<Items>
|
|
<DxAccordionItem Text='@L("SenderEditor_EnvelopeSettings", "Envelope-Einstellungen")' Expanded="false">
|
|
<ContentTemplate>
|
|
<div class="sender-settings-body">
|
|
<div class="sender-settings-grid">
|
|
<div class="sender-settings-field">
|
|
<label class="sender-settings-label">@L("EnvelopeType", "Umschlagtyp")</label>
|
|
<select value="@(_selectedEnvelopeTypeId?.ToString() ?? string.Empty)"
|
|
@onchange="OnEnvelopeTypeChanged"
|
|
class="sender-settings-select">
|
|
<option value="">@L("SenderEditor_SelectTemplate", "Vorlage auswählen")</option>
|
|
@foreach (var type in _envelopeTypes)
|
|
{
|
|
<option value="@type.Id">@type.Title</option>
|
|
}
|
|
</select>
|
|
</div>
|
|
|
|
<div class="sender-settings-field">
|
|
<label class="sender-settings-label">@L("Language", "Sprache")</label>
|
|
<select @bind="_language" class="sender-settings-select">
|
|
<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 class="sender-settings-field">
|
|
<label class="sender-settings-label">@L("CertificationType", "Zertifizierung")</label>
|
|
<select @bind="_certificationType" class="sender-settings-select">
|
|
<option value="@CertificationType.AdvancedElectronicSignature">@L("AdvancedElectronicSignature", "Advanced Electronic Signature")</option>
|
|
<option value="@CertificationType.ElectronicSignature">@L("ElectronicSignature", "Electronic Signature")</option>
|
|
</select>
|
|
</div>
|
|
|
|
<div class="sender-settings-toggles">
|
|
<label class="sender-settings-toggle">
|
|
<input type="checkbox" checked="@_tfaEnabled" @onchange="OnTfaChanged" />
|
|
@L("TFAEnabled", "2FA aktiviert")
|
|
</label>
|
|
<label class="sender-settings-toggle">
|
|
<input type="checkbox" checked="@_useAccessCode" @onchange="OnUseAccessCodeChanged" disabled="@_tfaEnabled" />
|
|
@L("UseAccessCode", "Zugriffscode verwenden")
|
|
</label>
|
|
</div>
|
|
|
|
<div class="sender-settings-field">
|
|
<label class="sender-settings-label">@L("FinalEmailToCreator", "Finale E-Mail an Ersteller")</label>
|
|
<select @bind="_finalEmailToCreator" class="sender-settings-select">
|
|
<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 class="sender-settings-field">
|
|
<label class="sender-settings-label">@L("FinalEmailToReceivers", "Finale E-Mail an Empfänger")</label>
|
|
<select @bind="_finalEmailToReceivers" class="sender-settings-select">
|
|
<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 class="sender-settings-toggles sender-settings-toggles--single">
|
|
<label class="sender-settings-toggle">
|
|
<input type="checkbox" checked="@_sendReminderEmails" @onchange="OnSendReminderEmailsChanged" />
|
|
@L("SendReminderEmails", "Erinnerungs-E-Mails senden")
|
|
</label>
|
|
</div>
|
|
|
|
<div class="sender-settings-field">
|
|
<label class="sender-settings-label">@L("FirstReminderDays", "Erste Erinnerung (Tage)")</label>
|
|
<InputNumber @bind-Value="_firstReminderDays" class="form-control form-control-sm sender-settings-number" disabled="@(!_sendReminderEmails)" />
|
|
</div>
|
|
|
|
<div class="sender-settings-field">
|
|
<label class="sender-settings-label">@L("ReminderIntervalDays", "Erinnerungsintervall (Tage)")</label>
|
|
<InputNumber @bind-Value="_reminderIntervalDays" class="form-control form-control-sm sender-settings-number" disabled="@(!_sendReminderEmails)" />
|
|
</div>
|
|
|
|
<div class="sender-settings-field">
|
|
<label class="sender-settings-label">@L("ExpiresWhenDays", "Läuft ab nach (Tage)")</label>
|
|
<InputNumber @bind-Value="_expiresWhenDays" class="form-control form-control-sm sender-settings-number" />
|
|
</div>
|
|
|
|
<div class="sender-settings-field">
|
|
<label class="sender-settings-label">@L("ExpiresWarningWhenDays", "Ablaufwarnung (Tage)")</label>
|
|
<InputNumber @bind-Value="_expiresWarningWhenDays" class="form-control form-control-sm sender-settings-number" />
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</ContentTemplate>
|
|
</DxAccordionItem>
|
|
</Items>
|
|
</DxAccordion>
|
|
</div>
|
|
|
|
<div class="sender-receivers-panel">
|
|
<div class="sender-receivers-list">
|
|
<button class="pdf-toolbar__btn pdf-toolbar__btn--signature-change sender-toolbar-action-btn sender-receivers-list__add-btn"
|
|
@onclick="OpenAddReceiverPopup"
|
|
title='@L("AddReceiver", "Empfänger hinzufügen")'>
|
|
<svg xmlns="http://www.w3.org/2000/svg" width="14" height="14" fill="currentColor" viewBox="0 0 16 16">
|
|
<path d="M8 1a.5.5 0 0 1 .5.5v6h6a.5.5 0 0 1 0 1h-6v6a.5.5 0 0 1-1 0v-6h-6a.5.5 0 0 1 0-1h6v-6A.5.5 0 0 1 8 1z" />
|
|
</svg>
|
|
<span class="sender-receivers-list__add-text">@L("AddReceiver", "Empfänger hinzufügen")</span>
|
|
<span class="badge rounded-pill text-bg-primary sender-receivers-list__count">@_receivers.Count</span>
|
|
</button>
|
|
|
|
@foreach (var receiver in _receivers)
|
|
{
|
|
<div class="sender-receiver-chip" style="--receiver-color:@receiver.Color;">
|
|
<div class="sender-receiver-chip__body">
|
|
<div class="sender-receiver-chip__email" title="@receiver.FullName">@receiver.Email</div>
|
|
</div>
|
|
|
|
<button class="pdf-toolbar__btn pdf-toolbar__btn--reset sender-toolbar-action-btn sender-toolbar-action-btn--compact sender-receiver-chip__action sender-receiver-chip__remove-btn"
|
|
@onclick="() => RemoveReceiverAsync(receiver)"
|
|
title='@L("Delete", "Loeschen")'>
|
|
<svg xmlns="http://www.w3.org/2000/svg" width="14" height="14" fill="currentColor" viewBox="0 0 16 16">
|
|
<path d="M4 4L12 12M12 4L4 12" stroke="currentColor" stroke-width="2" stroke-linecap="round" />
|
|
</svg>
|
|
</button>
|
|
|
|
<button class="pdf-toolbar__btn pdf-toolbar__btn--signature-change sender-toolbar-action-btn sender-toolbar-action-btn--compact sender-receiver-chip__action sender-receiver-chip__place-btn
|
|
@(_pendingReceiverForPlacement?.Id == receiver.Id ? "sender-receiver-chip__place-btn--active" : "sender-receiver-chip__place-btn--idle")"
|
|
@onclick="() => ActivatePlacementForReceiver(receiver)"
|
|
style="--receiver-color:@receiver.Color;"
|
|
title='@L("SenderEditor_PlaceSignatureField", "Signaturfeld platzieren")'>
|
|
<svg xmlns="http://www.w3.org/2000/svg" width="14" height="14" fill="currentColor" viewBox="0 0 16 16">
|
|
<path d="M12.146.146a.5.5 0 0 1 .708 0l3 3a.5.5 0 0 1 0 .708l-10 10a.5.5 0 0 1-.168.11l-5 2a.5.5 0 0 1-.65-.65l2-5a.5.5 0 0 1 .11-.168l10-10z" />
|
|
</svg>
|
|
</button>
|
|
</div>
|
|
}
|
|
</div>
|
|
|
|
@if (_receivers.Count == 0)
|
|
{
|
|
<div class="sender-receivers-panel__empty">
|
|
@L("SenderEditor_NoReceiversAdded", "Noch keine Empfänger hinzugefügt.")
|
|
</div>
|
|
}
|
|
</div>
|
|
</div>
|
|
|
|
@* Right: Buttons *@
|
|
<div class="sender-editor-actions">
|
|
|
|
<button class="pdf-toolbar__btn pdf-toolbar__btn--reset"
|
|
@onclick="Cancel"
|
|
title='@L("Cancel", "Abbrechen")'>
|
|
@L("Cancel", "Abbrechen")
|
|
</button>
|
|
|
|
@* PDF Upload *@
|
|
<label class="pdf-toolbar__btn sender-editor-upload-btn @((!_isPageReady || _isPdfUploadInProgress) ? "sender-editor-upload-btn--disabled" : string.Empty)"
|
|
title='@L("UploadPdf", "PDF hochladen")'
|
|
>
|
|
@if (_isPdfUploadInProgress)
|
|
{
|
|
<span class="spinner-border spinner-border-sm sender-editor-spinner" role="status"></span>
|
|
}
|
|
else
|
|
{
|
|
<svg xmlns="http://www.w3.org/2000/svg" width="14" height="14" fill="currentColor" viewBox="0 0 16 16">
|
|
<path d="M.5 9.9a.5.5 0 0 1 .5.5v2.5a1 1 0 0 0 1 1h12a1 1 0 0 0 1-1v-2.5a.5.5 0 0 1 1 0v2.5a2 2 0 0 1-2 2H2a2 2 0 0 1-2-2v-2.5a.5.5 0 0 1 .5-.5z" />
|
|
<path d="M7.646 1.146a.5.5 0 0 1 .708 0l3 3a.5.5 0 0 1-.708.708L8.5 2.707V11.5a.5.5 0 0 1-1 0V2.707L5.354 4.854a.5.5 0 1 1-.708-.708l3-3z" />
|
|
</svg>
|
|
}
|
|
@if (_isPdfUploadInProgress)
|
|
{
|
|
@L("UploadPdf", "PDF hochladen")
|
|
@:
|
|
@:...
|
|
}
|
|
else if (_isPageReady)
|
|
{
|
|
@L("UploadPdf", "PDF hochladen")
|
|
}
|
|
else
|
|
{
|
|
@L("Loading", "Wird geladen")
|
|
@:
|
|
@:...
|
|
}
|
|
<InputFile OnChange="OnPdfFileSelectedAsync"
|
|
accept=".pdf"
|
|
disabled="@(!_isPageReady || _isPdfUploadInProgress)"
|
|
class="sender-editor-hidden-input" />
|
|
</label>
|
|
|
|
@if (_pdfLoaded && _signatureFields.Count > 0)
|
|
{
|
|
<button class="pdf-toolbar__btn pdf-toolbar__btn--reset"
|
|
@onclick='() => _clearFieldsConfirmVisible = true'
|
|
title='@L("SenderEditor_RemoveAllSignatureFields", "Alle Signaturfelder entfernen")'>
|
|
<svg xmlns="http://www.w3.org/2000/svg" width="14" height="14" fill="currentColor" viewBox="0 0 16 16">
|
|
<path d="M5.5 5.5A.5.5 0 0 1 6 6v6a.5.5 0 0 1-1 0V6a.5.5 0 0 1 .5-.5zm2.5 0a.5.5 0 0 1 .5.5v6a.5.5 0 0 1-1 0V6a.5.5 0 0 1 .5-.5zm3 .5a.5.5 0 0 0-1 0v6a.5.5 0 0 0 1 0V6z" />
|
|
<path fill-rule="evenodd" d="M14.5 3a1 1 0 0 1-1 1H13v9a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2V4h-.5a1 1 0 0 1 0-2h3a1 1 0 0 1 1-1h3a1 1 0 0 1 1 1h3a1 1 0 0 1 1 1zM4.118 4 4 4.059V13a1 1 0 0 0 1 1h6a1 1 0 0 0 1-1V4.059L11.882 4H4.118zM2.5 3h11V2h-11v1z" />
|
|
</svg>
|
|
<span class="pdf-toolbar__btn-text"> @L("SenderEditor_SignatureField", "Signaturfeld")</span>
|
|
</button>
|
|
}
|
|
|
|
<button class="pdf-toolbar__btn pdf-toolbar__btn--draft"
|
|
@onclick="SaveDraftAsync"
|
|
disabled="@_isSaving"
|
|
title='@L("Save", "Speichern")'>
|
|
@if (_isSaving)
|
|
{
|
|
<span class="spinner-border spinner-border-sm me-1 sender-editor-spinner" role="status"></span>
|
|
}
|
|
else
|
|
{
|
|
<svg xmlns="http://www.w3.org/2000/svg" width="14" height="14" fill="currentColor" class="me-1" viewBox="0 0 16 16">
|
|
<path d="M2 2a2 2 0 0 0-2 2v8a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V5.414A2 2 0 0 0 15.414 4L12 0.586A2 2 0 0 0 10.586 0H2zm9 1.5v2A1.5 1.5 0 0 1 9.5 7h-3A1.5 1.5 0 0 1 5 5.5v-4h5.586a1 1 0 0 1 .707.293L14.207 4.707A1 1 0 0 1 14.5 5.414V12a.5.5 0 0 1-.5.5H2a.5.5 0 0 1-.5-.5V4A.5.5 0 0 1 2 3.5h9z"/>
|
|
<path d="M5.5 1.5v4a.5.5 0 0 0 .5.5h3a.5.5 0 0 0 .5-.5v-4h-4zM4 9a1 1 0 0 0-1 1v3h10v-3a1 1 0 0 0-1-1H4z"/>
|
|
</svg>
|
|
}
|
|
<span class="pdf-toolbar__btn-text">@L("Save", "Speichern")</span>
|
|
</button>
|
|
|
|
<button class="pdf-toolbar__btn pdf-toolbar__btn--success"
|
|
@onclick="RequestSendAsync"
|
|
disabled="@_isSaving"
|
|
title='@L("SendEnvelope", "Umschlag senden")'>
|
|
@if (_isSaving)
|
|
{
|
|
<span class="spinner-border spinner-border-sm me-1 sender-editor-spinner" role="status"></span>
|
|
}
|
|
else
|
|
{
|
|
<svg xmlns="http://www.w3.org/2000/svg" width="14" height="14" fill="currentColor" class="me-1" viewBox="0 0 16 16">
|
|
<path d="M13.854 3.646a.5.5 0 0 1 0 .708l-7 7a.5.5 0 0 1-.708 0l-3.5-3.5a.5.5 0 1 1 .708-.708L6.5 10.293l6.646-6.647a.5.5 0 0 1 .708 0z" />
|
|
</svg>
|
|
}
|
|
@L("Send", "Senden")
|
|
</button>
|
|
</div>
|
|
</div>
|
|
|
|
@* Placement mode hint bar *@
|
|
@if (_pendingReceiverForPlacement is not null)
|
|
{
|
|
<div class="sender-editor-placement-hint" style="--placement-color:@_pendingReceiverForPlacement.Color;">
|
|
📌 @L("SenderEditor_ClickDocumentFor", "Klicken Sie auf die Stelle im Dokument für") <strong>@_pendingReceiverForPlacement.FullName</strong>.
|
|
<button @onclick="CancelPlacement" class="sender-editor-placement-cancel">
|
|
@L("Cancel", "Abbrechen")
|
|
</button>
|
|
</div>
|
|
}
|
|
</div>
|
|
|
|
@* ── Content ── *@
|
|
<div class="envelope-content sender-editor-content">
|
|
@if (!_pdfLoaded)
|
|
{
|
|
@* Empty state *@
|
|
<div class="d-flex justify-content-center align-items-center h-100">
|
|
<div class="text-center sender-editor-empty-state">
|
|
<div class="sender-editor-empty-state__icon-wrap">
|
|
<svg xmlns="http://www.w3.org/2000/svg" width="36" height="36" fill="white" viewBox="0 0 16 16">
|
|
<path d="M14 4.5V14a2 2 0 0 1-2 2H4a2 2 0 0 1-2-2V2a2 2 0 0 1 2-2h5.5L14 4.5zm-3 0A1.5 1.5 0 0 1 9.5 3V1H4a1 1 0 0 0-1 1v12a1 1 0 0 0 1 1h8a1 1 0 0 0 1-1V4.5h-2z" />
|
|
</svg>
|
|
</div>
|
|
<h5 class="text-white mb-2">@L("SenderEditor_NoDocumentLoaded", "Kein Dokument geladen")</h5>
|
|
<p class="text-white mb-4 sender-editor-empty-state__description">
|
|
@L("SenderEditor_UploadPdfToPlaceFields", "Laden Sie eine PDF-Datei hoch, um Signaturfelder zu platzieren.")
|
|
</p>
|
|
<label class="btn btn-light btn-sm sender-editor-empty-state__upload @((!_isPageReady || _isPdfUploadInProgress) ? "sender-editor-empty-state__upload--disabled" : string.Empty)">
|
|
@if (_isPdfUploadInProgress)
|
|
{
|
|
<span class="spinner-border spinner-border-sm me-1 sender-editor-spinner" role="status"></span>
|
|
}
|
|
else
|
|
{
|
|
<svg xmlns="http://www.w3.org/2000/svg" width="14" height="14" fill="currentColor" class="me-1" viewBox="0 0 16 16">
|
|
<path d="M.5 9.9a.5.5 0 0 1 .5.5v2.5a1 1 0 0 0 1 1h12a1 1 0 0 0 1-1v-2.5a.5.5 0 0 1 1 0v2.5a2 2 0 0 1-2 2H2a2 2 0 0 1-2-2v-2.5a.5.5 0 0 1 .5-.5z" />
|
|
<path d="M7.646 1.146a.5.5 0 0 1 .708 0l3 3a.5.5 0 0 1-.708.708L8.5 2.707V11.5a.5.5 0 0 1-1 0V2.707L5.354 4.854a.5.5 0 1 1-.708-.708l3-3z" />
|
|
</svg>
|
|
}
|
|
@if (_isPdfUploadInProgress)
|
|
{
|
|
@L("UploadPdf", "PDF hochladen")
|
|
@:
|
|
@:...
|
|
}
|
|
else if (_isPageReady)
|
|
{
|
|
@L("UploadPdf", "PDF hochladen")
|
|
}
|
|
else
|
|
{
|
|
@L("Loading", "Wird geladen")
|
|
@:
|
|
@:...
|
|
}
|
|
<InputFile OnChange="OnPdfFileSelectedAsync"
|
|
accept=".pdf"
|
|
disabled="@(!_isPageReady || _isPdfUploadInProgress)"
|
|
class="sender-editor-hidden-input" />
|
|
</label>
|
|
</div>
|
|
</div>
|
|
}
|
|
else if (_errorMessage is not null)
|
|
{
|
|
<div class="error-container">
|
|
<div class="alert alert-danger shadow-lg m-4">
|
|
<strong>@L("Error", "Fehler"):</strong> @_errorMessage
|
|
</div>
|
|
</div>
|
|
}
|
|
else
|
|
{
|
|
<SenderPdfImageEditor IsPlacementMode="@(_pendingReceiverForPlacement is not null)"
|
|
PageCount="@_pageCount"
|
|
Pages="@_pagePreviews"
|
|
PdfDataUrl="@PdfDataUrl"
|
|
PdfFileName="@_fileName"
|
|
Fields="@FieldOverlays"
|
|
EditorClick="OnPdfAreaClickAsync"
|
|
MoveField="OnMoveFieldAsync"
|
|
RemoveField="OnRemoveFieldByIndexAsync" />
|
|
}
|
|
</div>
|
|
</div>
|
|
|
|
<DxPopup @bind-Visible="_receiverPopupVisible"
|
|
HeaderText='@L("AddReceiver", "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">@L("FullName", "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">@L("PhoneNumber", "Telefonnummer") (@L("Optional", "optional"))</div>
|
|
<DxTextBox Text="@_receiverDraftPhoneNumber"
|
|
TextChanged="OnReceiverPhoneNumberChanged"
|
|
NullText="+49 170 1234567"
|
|
ClearButtonDisplayMode="DataEditorClearButtonDisplayMode.Auto"
|
|
BindValueMode="BindValueMode.OnInput"
|
|
CssClass="w-100" />
|
|
</div>
|
|
|
|
<div class="sender-receiver-popup__field">
|
|
<div class="sender-receiver-popup__label">@L("EmailAddress", "E-Mail-Adresse")</div>
|
|
<div @onkeydown="OnReceiverEmailKeyDownAsync">
|
|
<DxTextBox Text="@_receiverDraftEmail"
|
|
TextChanged="OnReceiverEmailTextChangedAsync"
|
|
NullText="name@beispiel.de"
|
|
ClearButtonDisplayMode="DataEditorClearButtonDisplayMode.Auto"
|
|
BindValueMode="BindValueMode.OnInput"
|
|
CssClass="w-100" />
|
|
</div>
|
|
|
|
<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="@_selectedReceiverEmailSuggestion"
|
|
ValueChanged="OnReceiverEmailSuggestionCommittedAsync"
|
|
SelectionMode="ListBoxSelectionMode.Single"
|
|
CssClass="w-100" />
|
|
</div>
|
|
}
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="sender-receiver-popup__hint">
|
|
@L("SenderEditor_EmailSuggestionsHint", "Bereits verwendete E-Mail-Adressen werden bei der Eingabe vorgeschlagen.")
|
|
</div>
|
|
|
|
@if (_isReceiverEmailSearchRunning)
|
|
{
|
|
<div class="sender-receiver-popup__loading">@L("SenderEditor_EmailSuggestionsLoading", "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='@L("Cancel", "Abbrechen")'
|
|
RenderStyle="ButtonRenderStyle.Secondary"
|
|
Click="CloseAddReceiverPopup" />
|
|
<DxButton Text='@L("AddReceiver", "Empfänger hinzufügen")'
|
|
RenderStyle="ButtonRenderStyle.Primary"
|
|
Click="SaveReceiverAsync" />
|
|
</div>
|
|
</FooterContentTemplate>
|
|
</DxPopup>
|
|
|
|
@* ── Clear all fields confirmation popup ── *@
|
|
<DxPopup @bind-Visible="_clearFieldsConfirmVisible"
|
|
Width="400px"
|
|
MaxWidth="95vw"
|
|
ShowHeader="false"
|
|
ShowFooter="false"
|
|
CloseOnEscape="true"
|
|
CloseOnOutsideClick="true">
|
|
<BodyContentTemplate>
|
|
<div class="sender-confirm-popup">
|
|
<p class="sender-confirm-popup__message">@L("SenderEditor_RemoveAllSignatureFieldsConfirm", "Diese Aktion entfernt alle platzierten Signaturfelder unwiderruflich. Fortfahren?")</p>
|
|
<div class="sender-confirm-popup__actions">
|
|
<DxButton Text='@L("Cancel", "Abbrechen")'
|
|
RenderStyle="ButtonRenderStyle.Secondary"
|
|
Click='() => _clearFieldsConfirmVisible = false' />
|
|
<DxButton Text='@L("Delete", "Löschen")'
|
|
RenderStyle="ButtonRenderStyle.Danger"
|
|
Click="ClearAllFieldsAsync" />
|
|
</div>
|
|
</div>
|
|
</BodyContentTemplate>
|
|
</DxPopup>
|
|
|
|
@* ── Send confirmation popup ── *@
|
|
<DxPopup @bind-Visible="_sendConfirmVisible"
|
|
HeaderText='@L("SenderEditor_SendConfirm_Title", "Umschlag jetzt versenden?")'
|
|
SizeMode="SizeMode.Medium"
|
|
Width="460px"
|
|
MaxWidth="95vw"
|
|
ShowFooter="@(!_isSendConfirmSubmitting)"
|
|
ShowCloseButton="@(!_isSendConfirmSubmitting)"
|
|
CloseOnEscape="@(!_isSendConfirmSubmitting)"
|
|
CloseOnOutsideClick="false">
|
|
<BodyContentTemplate>
|
|
@if (_isSendConfirmSubmitting)
|
|
{
|
|
<div class="sender-send-progress">
|
|
<DxWaitIndicator Visible="true" />
|
|
<div>
|
|
<p class="sender-send-progress__title">@L("SenderEditor_SendConfirm_InProgress", "Umschlag wird versendet...")</p>
|
|
<p class="sender-send-progress__hint">@L("SenderEditor_SendConfirm_InProgressHint", "Bitte warten Sie einen Moment. Der Vorgang wird abgeschlossen.")</p>
|
|
</div>
|
|
</div>
|
|
}
|
|
else
|
|
{
|
|
<div class="sender-confirm-popup sender-confirm-popup--send">
|
|
<p class="sender-confirm-popup__note">
|
|
@L("SenderEditor_SendConfirm_Message", "Wollen Sie den Umschlag jetzt versenden? Hinweis: Nach der Übergabe kann der Vorgang nicht mehr bearbeitet werden.")
|
|
</p>
|
|
</div>
|
|
}
|
|
</BodyContentTemplate>
|
|
<FooterContentTemplate>
|
|
<DxButton Text='@L("No", "Nein")'
|
|
RenderStyle="ButtonRenderStyle.Secondary"
|
|
Click='() => _sendConfirmVisible = false' />
|
|
<DxButton Text='@L("SenderEditor_SendConfirm_Action", "Ja, versenden")'
|
|
RenderStyle="ButtonRenderStyle.Primary"
|
|
Click="ConfirmSendAsync" />
|
|
</FooterContentTemplate>
|
|
</DxPopup>
|
|
|
|
@* ── Save result popup (success + error) ── *@
|
|
<DxPopup @bind-Visible="_savePopupVisible"
|
|
HeaderText='@(_saveErrorMessage is null ? _saveSuccessHeader : L("Error", "Fehler"))'
|
|
Width="460px"
|
|
MaxWidth="95vw"
|
|
ShowFooter="true"
|
|
CloseOnOutsideClick="false"
|
|
ShowCloseButton="false"
|
|
CloseOnEscape="false">
|
|
<BodyContentTemplate>
|
|
@if (_saveErrorMessage is null)
|
|
{
|
|
<div class="sender-editor-save-result">
|
|
<div class="sender-editor-save-result__icon sender-editor-save-result__icon--success">
|
|
<svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" fill="#065f46" viewBox="0 0 16 16">
|
|
<path d="M13.854 3.646a.5.5 0 0 1 0 .708l-7 7a.5.5 0 0 1-.708 0l-3.5-3.5a.5.5 0 1 1 .708-.708L6.5 10.293l6.646-6.647a.5.5 0 0 1 .708 0z" />
|
|
</svg>
|
|
</div>
|
|
<div>
|
|
<p class="sender-editor-save-result__title">
|
|
@_saveSuccessTitle
|
|
</p>
|
|
<p class="sender-editor-save-result__message">
|
|
@_saveSuccessBody
|
|
</p>
|
|
</div>
|
|
</div>
|
|
}
|
|
else
|
|
{
|
|
<div class="sender-editor-save-result">
|
|
<div class="sender-editor-save-result__icon sender-editor-save-result__icon--error">
|
|
<svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" fill="#991b1b" viewBox="0 0 16 16">
|
|
<path d="M8 15A7 7 0 1 1 8 1a7 7 0 0 1 0 14zm0 1A8 8 0 1 0 8 0a8 8 0 0 0 0 16z" />
|
|
<path d="M7.002 11a1 1 0 1 1 2 0 1 1 0 0 1-2 0zM7.1 4.995a.905.905 0 1 1 1.8 0l-.35 3.507a.552.552 0 0 1-1.1 0L7.1 4.995z" />
|
|
</svg>
|
|
</div>
|
|
<div>
|
|
<p class="sender-editor-save-result__title">
|
|
@L("SenderEditor_ErrorCreatingEnvelope", "Fehler beim Erstellen des Umschlags")
|
|
</p>
|
|
<p class="sender-editor-save-result__message sender-editor-save-result__message--break">
|
|
@_saveErrorMessage
|
|
</p>
|
|
</div>
|
|
</div>
|
|
}
|
|
</BodyContentTemplate>
|
|
<FooterContentTemplate>
|
|
<div class="sender-editor-save-actions">
|
|
@if (_saveErrorMessage is null)
|
|
{
|
|
<button class="btn btn-outline-secondary sender-editor-save-actions__secondary"
|
|
@onclick="() => _savePopupVisible = false">
|
|
@L("Close", "Schließen")
|
|
</button>
|
|
|
|
@if (_saveNavigateToDashboard)
|
|
{
|
|
<button class="btn btn-primary sender-editor-save-actions__primary" @onclick="GoToDashboard">
|
|
@L("SenderEditor_ToOverview", "Zur Übersicht")
|
|
</button>
|
|
}
|
|
}
|
|
else
|
|
{
|
|
<button class="btn btn-outline-secondary sender-editor-save-actions__secondary"
|
|
@onclick="() => _savePopupVisible = false">
|
|
@L("Close", "Schließen")
|
|
</button>
|
|
}
|
|
</div>
|
|
</FooterContentTemplate>
|
|
</DxPopup>
|
|
|
|
@code {
|
|
[SupplyParameterFromQuery(Name = "envelopeId")]
|
|
public int? EnvelopeId { get; set; }
|
|
|
|
// ── Signature field size (configurable) ──
|
|
double SigWidthInches => SignatureFieldOptions.CurrentValue.Width;
|
|
double SigHeightInches => SignatureFieldOptions.CurrentValue.Height;
|
|
double SigWidthPt => SigWidthInches * 72.0;
|
|
double SigHeightPt => SigHeightInches * 72.0;
|
|
|
|
// CssClass for JS click coordinate helper
|
|
const string ViewerCssClass = "sender-editor-pdf-viewer";
|
|
|
|
// ── State ──
|
|
bool _pdfLoaded = false;
|
|
bool _isPageReady;
|
|
bool _isPdfUploadInProgress;
|
|
string _fileName = string.Empty;
|
|
string? _errorMessage;
|
|
byte[]? _originalPdfBytes; // Pristine upload — never modified, used as base for redraw
|
|
int _pageCount;
|
|
List<PdfPageImageDto> _pagePreviews = [];
|
|
int? _loadedEnvelopeId;
|
|
bool _isLoadingEnvelopeForEdit;
|
|
|
|
List<SignatureFieldDraft> _signatureFields = [];
|
|
ReceiverDraft? _pendingReceiverForPlacement; // Set when user clicks "Signatur hinzufügen"
|
|
|
|
IReadOnlyList<SenderPdfFieldOverlay> FieldOverlays => _signatureFields
|
|
.Select((f, i) => new SenderPdfFieldOverlay(i, f.Page, f.XPt, f.YPt, f.ReceiverName, f.Color))
|
|
.ToList();
|
|
|
|
string? PdfDataUrl => _originalPdfBytes is { Length: > 0 }
|
|
? $"data:application/pdf;base64,{Convert.ToBase64String(_originalPdfBytes)}"
|
|
: null;
|
|
|
|
// ── Save state ──
|
|
bool _isSaving = false;
|
|
bool _savePopupVisible = false;
|
|
bool _clearFieldsConfirmVisible = false;
|
|
bool _sendConfirmVisible = false;
|
|
bool _isSendConfirmSubmitting = false;
|
|
string? _saveErrorMessage = null;
|
|
string _saveSuccessHeader = "Erfolg";
|
|
string _saveSuccessTitle = "Vorgang erfolgreich abgeschlossen.";
|
|
string _saveSuccessBody = string.Empty;
|
|
bool _saveNavigateToDashboard = false;
|
|
|
|
// ── Envelope metadata ──
|
|
string _envelopeTitle = string.Empty;
|
|
string _envelopeMessage = string.Empty;
|
|
bool _titleTouched = false;
|
|
|
|
List<EnvelopeTypeDto> _envelopeTypes = [];
|
|
int? _selectedEnvelopeTypeId;
|
|
string _language = "de-DE";
|
|
bool _useAccessCode;
|
|
bool _tfaEnabled;
|
|
CertificationType _certificationType = 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 = [];
|
|
bool _receiverPopupVisible;
|
|
string _receiverDraftName = string.Empty;
|
|
string _receiverDraftEmail = string.Empty;
|
|
string _receiverDraftPhoneNumber = string.Empty;
|
|
string? _selectedReceiverEmailSuggestion;
|
|
string? _receiverPopupValidationMessage;
|
|
bool _isReceiverEmailSearchRunning;
|
|
List<string> _receiverEmailSuggestions = [];
|
|
int _receiverEmailSearchVersion;
|
|
|
|
static readonly System.ComponentModel.DataAnnotations.EmailAddressAttribute ReceiverEmailValidator = new();
|
|
|
|
// ── Lifecycle ──
|
|
|
|
protected override async Task OnInitializedAsync()
|
|
{
|
|
var hasAccess = await AuthService.CheckSenderAccessAsync();
|
|
if (!hasAccess)
|
|
{
|
|
NavigationManager.NavigateTo("/sender/login");
|
|
return;
|
|
}
|
|
|
|
try
|
|
{
|
|
_envelopeTypes = (await EnvelopeTypeService.GetAsync()).ToList();
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
Logger.LogWarning(ex, "Failed to load envelope types for sender editor.");
|
|
_envelopeTypes = [];
|
|
}
|
|
}
|
|
|
|
protected override async Task OnParametersSetAsync()
|
|
{
|
|
if (!EnvelopeId.HasValue || EnvelopeId.Value <= 0)
|
|
{
|
|
NavigationManager.NavigateTo("/sender", replace: true);
|
|
return;
|
|
}
|
|
|
|
if (_loadedEnvelopeId != EnvelopeId && !_isLoadingEnvelopeForEdit)
|
|
await LoadEnvelopeForEditAsync(EnvelopeId.Value);
|
|
}
|
|
|
|
protected override async Task OnAfterRenderAsync(bool firstRender)
|
|
{
|
|
if (firstRender)
|
|
{
|
|
_isPageReady = true;
|
|
await InvokeAsync(StateHasChanged);
|
|
}
|
|
}
|
|
|
|
async Task LoadEnvelopeForEditAsync(int envelopeId)
|
|
{
|
|
if (_isLoadingEnvelopeForEdit)
|
|
return;
|
|
|
|
_isLoadingEnvelopeForEdit = true;
|
|
_errorMessage = null;
|
|
|
|
try
|
|
{
|
|
var envelopes = await EnvelopeService.GetAsync(id: envelopeId);
|
|
var envelope = envelopes?.FirstOrDefault()
|
|
?? throw new InvalidOperationException($"Envelope {envelopeId} not found.");
|
|
|
|
var document = await DocumentService.GetSenderDocumentDetailsByEnvelopeIdAsync(envelopeId);
|
|
|
|
_envelopeTitle = envelope.Title ?? 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 ?? CertificationType.AdvancedElectronicSignature;
|
|
_finalEmailToCreator = envelope.FinalEmailToCreator ?? (int)FinalEmailType.No;
|
|
_finalEmailToReceivers = envelope.FinalEmailToReceivers ?? (int)FinalEmailType.No;
|
|
|
|
_sendReminderEmails = envelope.SendReminderEmails ?? envelope.EnvelopeType?.SendReminderEmails ?? false;
|
|
_firstReminderDays = envelope.FirstReminderDays ?? envelope.EnvelopeType?.FirstReminderDays ?? 0;
|
|
_reminderIntervalDays = envelope.ReminderIntervalDays ?? envelope.EnvelopeType?.ReminderIntervalDays ?? 0;
|
|
_expiresWhenDays = envelope.ExpiresWhenDays ?? envelope.EnvelopeType?.ExpiresDays ?? 0;
|
|
_expiresWarningWhenDays = envelope.ExpiresWarningWhenDays ?? envelope.EnvelopeType?.ExpiresWarningDays ?? 0;
|
|
|
|
NormalizeSettings();
|
|
|
|
var envelopeReceivers = envelope.EnvelopeReceivers?.ToList() ?? [];
|
|
_receivers = envelopeReceivers
|
|
.Select((er, index) => new ReceiverDraft(
|
|
Guid.NewGuid(),
|
|
er.ReceiverId,
|
|
er.Name ?? er.Receiver?.EmailAddress ?? $"Receiver {er.ReceiverId}",
|
|
er.Receiver?.EmailAddress ?? string.Empty,
|
|
string.Empty,
|
|
ReceiverPalette[index % ReceiverPalette.Length]))
|
|
.ToList();
|
|
|
|
var receiverDraftByReceiverId = _receivers
|
|
.Where(r => r.ReceiverId.HasValue)
|
|
.ToDictionary(r => r.ReceiverId!.Value, r => r);
|
|
|
|
var receiverById = envelopeReceivers.ToDictionary(er => er.ReceiverId, er => er);
|
|
var colorByEmail = _receivers
|
|
.Where(r => !string.IsNullOrWhiteSpace(r.Email))
|
|
.GroupBy(r => r.Email, StringComparer.OrdinalIgnoreCase)
|
|
.ToDictionary(g => g.Key, g => g.First().Color, StringComparer.OrdinalIgnoreCase);
|
|
|
|
_signatureFields = (document?.Elements ?? Enumerable.Empty<DocReceiverElementDto>())
|
|
.Select(element =>
|
|
{
|
|
receiverById.TryGetValue(element.ReceiverId, out var envelopeReceiver);
|
|
|
|
var receiverName = envelopeReceiver?.Name
|
|
?? envelopeReceiver?.Receiver?.EmailAddress
|
|
?? element.FullName
|
|
?? $"Receiver {element.ReceiverId}";
|
|
|
|
var receiverEmail = envelopeReceiver?.Receiver?.EmailAddress;
|
|
var color = (!string.IsNullOrWhiteSpace(receiverEmail) && colorByEmail.TryGetValue(receiverEmail, out var receiverColor))
|
|
? receiverColor
|
|
: ReceiverPalette[Math.Abs(element.ReceiverId) % ReceiverPalette.Length];
|
|
|
|
var receiverDraft = receiverDraftByReceiverId.TryGetValue(element.ReceiverId, out var draft)
|
|
? draft
|
|
: _receivers.FirstOrDefault(r =>
|
|
(!string.IsNullOrWhiteSpace(receiverEmail) && string.Equals(r.Email, receiverEmail, StringComparison.OrdinalIgnoreCase))
|
|
|| string.Equals(r.FullName, receiverName, StringComparison.OrdinalIgnoreCase));
|
|
|
|
return new SignatureFieldDraft(
|
|
XPt: element.X * 72.0,
|
|
YPt: element.Y * 72.0,
|
|
Page: element.Page,
|
|
ReceiverDraftId: receiverDraft?.Id ?? Guid.Empty,
|
|
ReceiverName: receiverName,
|
|
Color: color);
|
|
})
|
|
.ToList();
|
|
|
|
_originalPdfBytes = document?.ByteData;
|
|
_pdfLoaded = _originalPdfBytes is { Length: > 0 };
|
|
|
|
if (_pdfLoaded)
|
|
{
|
|
try
|
|
{
|
|
_pagePreviews = (await PdfRenderService.RenderPagesAsync(_originalPdfBytes!))
|
|
.OrderBy(p => p.PageNumber)
|
|
.ToList();
|
|
}
|
|
catch (Exception renderEx)
|
|
{
|
|
Logger.LogWarning(renderEx, "Failed to render PDF page previews for envelope {EnvelopeId}. Falling back to native preview.", envelopeId);
|
|
_pagePreviews = [];
|
|
}
|
|
|
|
_pageCount = _pagePreviews.Count > 0
|
|
? _pagePreviews.Count
|
|
: 1;
|
|
}
|
|
else
|
|
{
|
|
_pagePreviews = [];
|
|
_pageCount = Math.Max(1, _signatureFields.Select(f => f.Page).DefaultIfEmpty(1).Max());
|
|
}
|
|
|
|
_fileName = $"Envelope-{envelopeId}.pdf";
|
|
_pendingReceiverForPlacement = null;
|
|
_loadedEnvelopeId = envelopeId;
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
Logger.LogError(ex, "Failed to load envelope {EnvelopeId} for edit mode", envelopeId);
|
|
_errorMessage = string.Format(
|
|
L("SenderEditor_LoadEnvelopeError", "Fehler beim Laden des Umschlags #{0}: {1}"),
|
|
envelopeId,
|
|
ex.Message);
|
|
}
|
|
finally
|
|
{
|
|
_isLoadingEnvelopeForEdit = false;
|
|
}
|
|
}
|
|
|
|
// ── PDF upload ──
|
|
async Task OnPdfFileSelectedAsync(InputFileChangeEventArgs e)
|
|
{
|
|
if (!_isPageReady)
|
|
return;
|
|
|
|
if (_isPdfUploadInProgress)
|
|
return;
|
|
|
|
_errorMessage = null;
|
|
var file = e.File;
|
|
if (file is null) return;
|
|
|
|
if (!file.Name.EndsWith(".pdf", StringComparison.OrdinalIgnoreCase))
|
|
{
|
|
_errorMessage = L("SenderEditor_SelectPdf", "Bitte wählen Sie eine PDF-Datei aus.");
|
|
return;
|
|
}
|
|
|
|
try
|
|
{
|
|
_isPdfUploadInProgress = true;
|
|
await InvokeAsync(StateHasChanged);
|
|
|
|
const long maxBytes = 50 * 1024 * 1024;
|
|
using var ms = new System.IO.MemoryStream();
|
|
await file.OpenReadStream(maxBytes).CopyToAsync(ms);
|
|
|
|
_originalPdfBytes = ms.ToArray();
|
|
_fileName = file.Name;
|
|
_pdfLoaded = true;
|
|
_pagePreviews = (await PdfRenderService.RenderPagesAsync(_originalPdfBytes))
|
|
.OrderBy(p => p.PageNumber)
|
|
.ToList();
|
|
_pageCount = _pagePreviews.Count > 0
|
|
? _pagePreviews.Count
|
|
: EstimatePdfPageCount(_originalPdfBytes) ?? 1;
|
|
_signatureFields.Clear();
|
|
_pendingReceiverForPlacement = null;
|
|
|
|
Logger.LogInformation("PDF loaded: {Name} ({Size} bytes)", _fileName, _originalPdfBytes.Length);
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
_errorMessage = string.Format(
|
|
L("SenderEditor_LoadFileError", "Fehler beim Laden der Datei: {0}"),
|
|
ex.Message);
|
|
Logger.LogError(ex, "Failed to load PDF file");
|
|
}
|
|
finally
|
|
{
|
|
_isPdfUploadInProgress = false;
|
|
await InvokeAsync(StateHasChanged);
|
|
}
|
|
}
|
|
|
|
// ── Placement mode ──
|
|
void ActivatePlacementForReceiver(ReceiverDraft receiver)
|
|
{
|
|
// Toggle: clicking the same receiver again cancels placement
|
|
_pendingReceiverForPlacement = _pendingReceiverForPlacement?.Id == receiver.Id
|
|
? null
|
|
: receiver;
|
|
}
|
|
|
|
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 = ParseCheckboxValue(e);
|
|
if (_tfaEnabled)
|
|
_useAccessCode = true;
|
|
}
|
|
|
|
void OnUseAccessCodeChanged(ChangeEventArgs e)
|
|
{
|
|
if (_tfaEnabled)
|
|
{
|
|
_useAccessCode = true;
|
|
return;
|
|
}
|
|
|
|
_useAccessCode = ParseCheckboxValue(e);
|
|
}
|
|
|
|
void OnSendReminderEmailsChanged(ChangeEventArgs e)
|
|
{
|
|
_sendReminderEmails = ParseCheckboxValue(e);
|
|
}
|
|
|
|
static bool ParseCheckboxValue(ChangeEventArgs e)
|
|
{
|
|
if (e.Value is bool b)
|
|
return b;
|
|
|
|
return bool.TryParse(e.Value?.ToString(), out var parsed) && parsed;
|
|
}
|
|
|
|
// ── PDF area click → place field ──
|
|
async Task OnPdfAreaClickAsync(MouseEventArgs e)
|
|
{
|
|
if (_pendingReceiverForPlacement is null) return;
|
|
if (!_pdfLoaded || _originalPdfBytes is null) return;
|
|
|
|
// Ask JS for the normalised click position within the rendered PDF page
|
|
var coords = await JSRuntime.InvokeAsync<NormalisedCoords?>(
|
|
"envelopeEditor.getClickCoordsOnPdfPage",
|
|
ViewerCssClass, e.ClientX, e.ClientY);
|
|
|
|
if (coords is null)
|
|
{
|
|
Logger.LogWarning("[SenderEditor] getClickCoordsOnPdfPage returned null");
|
|
return;
|
|
}
|
|
|
|
int page1Based = coords.PageIndex + 1;
|
|
var pagePreview = _pagePreviews.FirstOrDefault(p => p.PageNumber == page1Based);
|
|
var pageWidthPt = pagePreview?.WidthPt ?? 595.0;
|
|
var pageHeightPt = pagePreview?.HeightPt ?? 842.0;
|
|
|
|
// Convert normalised [0,1] → PDF points; clamp so box stays inside page
|
|
double xPt = coords.NormX * pageWidthPt;
|
|
double yPt = coords.NormY * pageHeightPt;
|
|
|
|
xPt = Math.Max(0, Math.Min(xPt, pageWidthPt - SigWidthPt));
|
|
yPt = Math.Max(0, Math.Min(yPt, pageHeightPt - SigHeightPt));
|
|
|
|
var field = new SignatureFieldDraft(
|
|
XPt: xPt,
|
|
YPt: yPt,
|
|
Page: page1Based,
|
|
ReceiverDraftId: _pendingReceiverForPlacement.Id,
|
|
ReceiverName: _pendingReceiverForPlacement.FullName,
|
|
Color: _pendingReceiverForPlacement.Color);
|
|
|
|
_signatureFields.Add(field);
|
|
_pendingReceiverForPlacement = null;
|
|
|
|
Logger.LogInformation(
|
|
"[SenderEditor] Field added: Page={Page} X={X:F1}pt Y={Y:F1}pt Receiver={Receiver}",
|
|
page1Based, xPt, yPt, field.ReceiverName);
|
|
}
|
|
|
|
// ── Remove a single field ──
|
|
async Task OnRemoveFieldByIndexAsync(int index)
|
|
{
|
|
if (index < 0 || index >= _signatureFields.Count)
|
|
return;
|
|
|
|
_signatureFields.RemoveAt(index);
|
|
await Task.CompletedTask;
|
|
}
|
|
|
|
async Task OnMoveFieldAsync(SenderPdfFieldMoveRequest request)
|
|
{
|
|
if (request.Index < 0 || request.Index >= _signatureFields.Count)
|
|
return;
|
|
|
|
var field = _signatureFields[request.Index];
|
|
_signatureFields[request.Index] = field with
|
|
{
|
|
XPt = request.XPt,
|
|
YPt = request.YPt,
|
|
Page = request.Page
|
|
};
|
|
|
|
await Task.CompletedTask;
|
|
}
|
|
|
|
// ── Clear all fields ──
|
|
async Task ClearAllFieldsAsync()
|
|
{
|
|
_clearFieldsConfirmVisible = false;
|
|
_signatureFields.Clear();
|
|
_pendingReceiverForPlacement = null;
|
|
await Task.CompletedTask;
|
|
}
|
|
|
|
static int? EstimatePdfPageCount(byte[]? documentBytes)
|
|
{
|
|
if (documentBytes is null || documentBytes.Length == 0)
|
|
return null;
|
|
|
|
try
|
|
{
|
|
var text = System.Text.Encoding.ASCII.GetString(documentBytes);
|
|
const string marker = "/Type /Page";
|
|
var count = 0;
|
|
var index = 0;
|
|
|
|
while (true)
|
|
{
|
|
index = text.IndexOf(marker, index, StringComparison.Ordinal);
|
|
if (index < 0)
|
|
break;
|
|
|
|
count++;
|
|
index += marker.Length;
|
|
}
|
|
|
|
return count > 0 ? count : null;
|
|
}
|
|
catch
|
|
{
|
|
return null;
|
|
}
|
|
}
|
|
|
|
void Cancel() => NavigationManager.NavigateTo("/sender");
|
|
|
|
void GoToDashboard()
|
|
{
|
|
NavigationManager.NavigateTo("/sender");
|
|
}
|
|
|
|
async Task SaveDraftAsync()
|
|
{
|
|
await SaveDraftCoreAsync(navigateToDashboard: true);
|
|
}
|
|
|
|
async Task<int?> SaveDraftCoreAsync(bool navigateToDashboard)
|
|
{
|
|
if (!ValidateSettings(out var settingsError))
|
|
{
|
|
_saveErrorMessage = settingsError;
|
|
_savePopupVisible = true;
|
|
return null;
|
|
}
|
|
|
|
_isSaving = true;
|
|
_saveErrorMessage = null;
|
|
await InvokeAsync(StateHasChanged);
|
|
|
|
try
|
|
{
|
|
var command = BuildCreateEnvelopeCommand(send: false);
|
|
Logger.LogInformation("[SenderEditor] Saving draft. EnvelopeId={EnvelopeId}", command.EnvelopeId);
|
|
var result = await EnvelopeService.UpsertAsync(command);
|
|
|
|
if (result is null)
|
|
throw new InvalidOperationException("Draft save did not return a result.");
|
|
|
|
EnvelopeId = result.Id;
|
|
_loadedEnvelopeId = result.Id;
|
|
|
|
if (navigateToDashboard)
|
|
NavigationManager.NavigateTo("/sender", forceLoad: false);
|
|
|
|
return result.Id;
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
Logger.LogError(ex, "[SenderEditor] Failed to save draft");
|
|
_saveErrorMessage = ex.Message;
|
|
_savePopupVisible = true;
|
|
return null;
|
|
}
|
|
finally
|
|
{
|
|
_isSaving = false;
|
|
await InvokeAsync(StateHasChanged);
|
|
}
|
|
}
|
|
|
|
// ── Send envelope ──
|
|
async Task SendAsync()
|
|
{
|
|
if (!ValidateSettings(out var settingsError))
|
|
{
|
|
_saveErrorMessage = settingsError;
|
|
_savePopupVisible = true;
|
|
return;
|
|
}
|
|
|
|
// ── Validation ──
|
|
_titleTouched = true;
|
|
if (!_pdfLoaded || _originalPdfBytes is null)
|
|
{
|
|
_saveErrorMessage = L("SenderEditor_Validation_UploadPdf", "Bitte laden Sie zuerst ein PDF-Dokument hoch.");
|
|
_savePopupVisible = true;
|
|
return;
|
|
}
|
|
if (string.IsNullOrWhiteSpace(_envelopeTitle))
|
|
{
|
|
_saveErrorMessage = L("SenderEditor_Validation_TitleRequired", "Bitte geben Sie einen Titel ein.");
|
|
_savePopupVisible = true;
|
|
return;
|
|
}
|
|
if (_receivers.Count == 0)
|
|
{
|
|
_saveErrorMessage = L("SenderEditor_Validation_ReceiverRequired", "Bitte fügen Sie mindestens einen Empfänger hinzu.");
|
|
_savePopupVisible = true;
|
|
return;
|
|
}
|
|
if (_signatureFields.Count == 0)
|
|
{
|
|
_saveErrorMessage = L("SenderEditor_Validation_SignatureFieldRequired", "Bitte platzieren Sie mindestens ein Signaturfeld.");
|
|
_savePopupVisible = true;
|
|
return;
|
|
}
|
|
|
|
// Warn if any receiver has no signature field, but don't block
|
|
var receiversWithoutField = _receivers
|
|
.Where(r => !_signatureFields.Any(f => f.ReceiverDraftId == r.Id))
|
|
.ToList();
|
|
if (receiversWithoutField.Count > 0)
|
|
{
|
|
Logger.LogWarning(
|
|
"[SenderEditor] Receivers without signature field: {Names}",
|
|
string.Join(", ", receiversWithoutField.Select(r => r.FullName)));
|
|
}
|
|
|
|
_isSaving = true;
|
|
_saveErrorMessage = null;
|
|
await InvokeAsync(StateHasChanged);
|
|
|
|
try
|
|
{
|
|
var envelopeId = EnvelopeId ?? _loadedEnvelopeId;
|
|
|
|
if (!envelopeId.HasValue || envelopeId.Value <= 0)
|
|
throw new InvalidOperationException("Envelope id is missing for send.");
|
|
|
|
var sendResult = await EnvelopeService.UpsertAsync(BuildCreateEnvelopeCommand(send: true) with { EnvelopeId = envelopeId })
|
|
?? throw new InvalidOperationException("Send did not return a result.");
|
|
|
|
Logger.LogInformation("[SenderEditor] Envelope sent from draft. Id={Id} Status={Status}", sendResult.Id, sendResult.Status);
|
|
|
|
NavigationManager.NavigateTo("/sender", forceLoad: false);
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
Logger.LogError(ex, "[SenderEditor] Failed to send envelope draft");
|
|
_saveErrorMessage = ex.Message;
|
|
_savePopupVisible = true;
|
|
}
|
|
finally
|
|
{
|
|
_isSaving = false;
|
|
}
|
|
}
|
|
|
|
Task RequestSendAsync()
|
|
{
|
|
if (_isSaving)
|
|
return Task.CompletedTask;
|
|
|
|
_sendConfirmVisible = true;
|
|
return Task.CompletedTask;
|
|
}
|
|
|
|
async Task ConfirmSendAsync()
|
|
{
|
|
if (_isSendConfirmSubmitting)
|
|
return;
|
|
|
|
_isSendConfirmSubmitting = true;
|
|
await InvokeAsync(StateHasChanged);
|
|
|
|
try
|
|
{
|
|
await SendAsync();
|
|
}
|
|
finally
|
|
{
|
|
_isSendConfirmSubmitting = false;
|
|
|
|
if (_savePopupVisible)
|
|
_sendConfirmVisible = false;
|
|
|
|
await InvokeAsync(StateHasChanged);
|
|
}
|
|
}
|
|
|
|
CreateEnvelopeCommand BuildCreateEnvelopeCommand(bool send)
|
|
{
|
|
NormalizeSettings();
|
|
|
|
var envelopeId = EnvelopeId ?? _loadedEnvelopeId;
|
|
|
|
if (envelopeId is null)
|
|
throw new InvalidOperationException("Envelope id is missing.");
|
|
|
|
var docBase64 = _originalPdfBytes is { Length: > 0 }
|
|
? Convert.ToBase64String(_originalPdfBytes)
|
|
: null;
|
|
|
|
var receiverCommands = _receivers.Select(receiver =>
|
|
{
|
|
var fields = _signatureFields
|
|
.Where(f => f.ReceiverDraftId == receiver.Id)
|
|
.Select(f => new CreateEnvelopeFieldDto(
|
|
X: f.XPt / 72.0,
|
|
Y: f.YPt / 72.0,
|
|
Page: f.Page))
|
|
.ToList();
|
|
|
|
return new CreateEnvelopeReceiverDto(
|
|
EmailAddress: receiver.Email,
|
|
Name: receiver.FullName,
|
|
PhoneNumber: string.IsNullOrWhiteSpace(receiver.PhoneNumber) ? null : receiver.PhoneNumber,
|
|
Fields: fields);
|
|
}).ToList();
|
|
|
|
return new CreateEnvelopeCommand
|
|
{
|
|
EnvelopeId = envelopeId,
|
|
EnvelopeTypeId = _selectedEnvelopeTypeId,
|
|
Title = _envelopeTitle,
|
|
Message = _envelopeMessage,
|
|
Language = _language,
|
|
UseAccessCode = _useAccessCode,
|
|
TFAEnabled = _tfaEnabled,
|
|
CertificationType = _certificationType,
|
|
FinalEmailToCreator = _finalEmailToCreator,
|
|
FinalEmailToReceivers = _finalEmailToReceivers,
|
|
SendReminderEmails = _sendReminderEmails,
|
|
FirstReminderDays = _firstReminderDays,
|
|
ReminderIntervalDays = _reminderIntervalDays,
|
|
ExpiresWhenDays = _expiresWhenDays,
|
|
ExpiresWarningWhenDays = _expiresWarningWhenDays,
|
|
Send = send,
|
|
Document = string.IsNullOrWhiteSpace(docBase64) ? null : new CreateEnvelopeDocumentDto(docBase64),
|
|
Receivers = receiverCommands
|
|
};
|
|
}
|
|
|
|
void NormalizeSettings()
|
|
{
|
|
_language = string.IsNullOrWhiteSpace(_language) ? "de-DE" : _language.Trim();
|
|
|
|
if (_tfaEnabled)
|
|
_useAccessCode = true;
|
|
|
|
_firstReminderDays = Math.Max(0, _firstReminderDays);
|
|
_reminderIntervalDays = Math.Max(0, _reminderIntervalDays);
|
|
_expiresWhenDays = Math.Max(0, _expiresWhenDays);
|
|
_expiresWarningWhenDays = Math.Max(0, _expiresWarningWhenDays);
|
|
|
|
if (_expiresWhenDays > 0 && _expiresWarningWhenDays > _expiresWhenDays)
|
|
_expiresWarningWhenDays = _expiresWhenDays;
|
|
}
|
|
|
|
bool ValidateSettings(out string? message)
|
|
{
|
|
NormalizeSettings();
|
|
|
|
if (_sendReminderEmails && _reminderIntervalDays <= 0)
|
|
{
|
|
message = L("SenderEditor_Validation_ReminderInterval", "Bitte geben Sie ein Erinnerungsintervall groesser als 0 Tage ein.");
|
|
return false;
|
|
}
|
|
|
|
if (_sendReminderEmails && _firstReminderDays < 0)
|
|
{
|
|
message = L("SenderEditor_Validation_FirstReminder", "Bitte geben Sie gueltige Tage fuer die erste Erinnerung ein.");
|
|
return false;
|
|
}
|
|
|
|
if (_expiresWhenDays > 0 && _expiresWarningWhenDays > _expiresWhenDays)
|
|
{
|
|
message = L("SenderEditor_Validation_ExpiryWarning", "Die Ablaufwarnung darf nicht nach dem Ablaufdatum liegen.");
|
|
return false;
|
|
}
|
|
|
|
message = null;
|
|
return true;
|
|
}
|
|
|
|
// ── Receiver popup ──
|
|
void OpenAddReceiverPopup()
|
|
{
|
|
_receiverDraftName = string.Empty;
|
|
_receiverDraftEmail = string.Empty;
|
|
_receiverDraftPhoneNumber = string.Empty;
|
|
_selectedReceiverEmailSuggestion = null;
|
|
_receiverPopupValidationMessage = null;
|
|
_receiverEmailSuggestions.Clear();
|
|
_receiverPopupVisible = true;
|
|
}
|
|
|
|
void CloseAddReceiverPopup()
|
|
{
|
|
_receiverPopupVisible = false;
|
|
_receiverPopupValidationMessage = null;
|
|
_selectedReceiverEmailSuggestion = null;
|
|
_isReceiverEmailSearchRunning = false;
|
|
}
|
|
|
|
void OnReceiverNameChanged(string? value)
|
|
{
|
|
_receiverDraftName = value ?? string.Empty;
|
|
_receiverPopupValidationMessage = null;
|
|
}
|
|
|
|
void OnReceiverPhoneNumberChanged(string? value)
|
|
{
|
|
_receiverDraftPhoneNumber = value ?? string.Empty;
|
|
_receiverPopupValidationMessage = null;
|
|
}
|
|
|
|
async Task OnReceiverEmailKeyDownAsync(KeyboardEventArgs e)
|
|
{
|
|
if (_receiverEmailSuggestions.Count == 0)
|
|
{
|
|
if (e.Key == "Escape") _selectedReceiverEmailSuggestion = null;
|
|
return;
|
|
}
|
|
|
|
var currentIndex = _selectedReceiverEmailSuggestion is null
|
|
? -1
|
|
: _receiverEmailSuggestions.FindIndex(em =>
|
|
string.Equals(em, _selectedReceiverEmailSuggestion, StringComparison.OrdinalIgnoreCase));
|
|
|
|
if (e.Key == "ArrowDown")
|
|
{
|
|
var next = currentIndex < _receiverEmailSuggestions.Count - 1 ? currentIndex + 1 : 0;
|
|
SelectReceiverEmailSuggestion(_receiverEmailSuggestions[next]);
|
|
}
|
|
else if (e.Key == "ArrowUp")
|
|
{
|
|
var next = currentIndex > 0 ? currentIndex - 1 : _receiverEmailSuggestions.Count - 1;
|
|
SelectReceiverEmailSuggestion(_receiverEmailSuggestions[next]);
|
|
}
|
|
else if (e.Key == "Enter")
|
|
{
|
|
var sel = currentIndex >= 0 && currentIndex < _receiverEmailSuggestions.Count
|
|
? _receiverEmailSuggestions[currentIndex]
|
|
: _receiverEmailSuggestions.FirstOrDefault();
|
|
if (!string.IsNullOrWhiteSpace(sel))
|
|
await OnReceiverEmailSuggestionCommittedAsync(sel);
|
|
}
|
|
else if (e.Key == "Escape")
|
|
{
|
|
_receiverEmailSuggestions.Clear();
|
|
_selectedReceiverEmailSuggestion = null;
|
|
}
|
|
}
|
|
|
|
void SelectReceiverEmailSuggestion(string? value)
|
|
{
|
|
if (string.IsNullOrWhiteSpace(value)) return;
|
|
_selectedReceiverEmailSuggestion = value.Trim();
|
|
_receiverDraftEmail = _selectedReceiverEmailSuggestion;
|
|
_receiverPopupValidationMessage = null;
|
|
}
|
|
|
|
Task OnReceiverEmailSuggestionCommittedAsync(string? value)
|
|
{
|
|
SelectReceiverEmailSuggestion(value);
|
|
_receiverEmailSuggestions.Clear();
|
|
return Task.CompletedTask;
|
|
}
|
|
|
|
async Task OnReceiverEmailTextChangedAsync(string? value)
|
|
{
|
|
_receiverDraftEmail = value?.Trim() ?? string.Empty;
|
|
_selectedReceiverEmailSuggestion = _receiverDraftEmail;
|
|
_receiverPopupValidationMessage = null;
|
|
|
|
var searchVersion = ++_receiverEmailSearchVersion;
|
|
|
|
if (string.IsNullOrWhiteSpace(_receiverDraftEmail) || _receiverDraftEmail.Length < 2)
|
|
{
|
|
_receiverEmailSuggestions.Clear();
|
|
_selectedReceiverEmailSuggestion = null;
|
|
_isReceiverEmailSearchRunning = false;
|
|
return;
|
|
}
|
|
|
|
_isReceiverEmailSearchRunning = true;
|
|
|
|
try
|
|
{
|
|
var results = await ReceiverService.SearchReceiverEmailsAsync(_receiverDraftEmail);
|
|
if (searchVersion != _receiverEmailSearchVersion) return;
|
|
|
|
_receiverEmailSuggestions = results
|
|
.Where(em => !string.IsNullOrWhiteSpace(em))
|
|
.Distinct(StringComparer.OrdinalIgnoreCase)
|
|
.OrderBy(em => em)
|
|
.Take(12)
|
|
.ToList();
|
|
|
|
_selectedReceiverEmailSuggestion = _receiverEmailSuggestions.FirstOrDefault(em =>
|
|
string.Equals(em, _receiverDraftEmail, StringComparison.OrdinalIgnoreCase));
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
Logger.LogWarning(ex, "Failed to load receiver email suggestions for {SearchTerm}", _receiverDraftEmail);
|
|
if (searchVersion == _receiverEmailSearchVersion)
|
|
{
|
|
_receiverEmailSuggestions.Clear();
|
|
_selectedReceiverEmailSuggestion = null;
|
|
}
|
|
}
|
|
finally
|
|
{
|
|
if (searchVersion == _receiverEmailSearchVersion)
|
|
_isReceiverEmailSearchRunning = false;
|
|
}
|
|
}
|
|
|
|
Task SaveReceiverAsync()
|
|
{
|
|
var fullName = _receiverDraftName.Trim();
|
|
var email = _receiverDraftEmail.Trim();
|
|
var phoneNumber = _receiverDraftPhoneNumber.Trim();
|
|
|
|
if (string.IsNullOrWhiteSpace(fullName))
|
|
{
|
|
_receiverPopupValidationMessage = L("SenderEditor_Validation_FullNameRequired", "Bitte geben Sie einen Vor- und Nachnamen ein.");
|
|
return Task.CompletedTask;
|
|
}
|
|
if (string.IsNullOrWhiteSpace(email))
|
|
{
|
|
_receiverPopupValidationMessage = L("SenderEditor_Validation_EmailRequired", "Bitte geben Sie eine E-Mail-Adresse ein.");
|
|
return Task.CompletedTask;
|
|
}
|
|
if (!ReceiverEmailValidator.IsValid(email))
|
|
{
|
|
_receiverPopupValidationMessage = L("SenderEditor_Validation_EmailInvalid", "Bitte geben Sie eine gültige E-Mail-Adresse ein.");
|
|
return Task.CompletedTask;
|
|
}
|
|
if (_receivers.Any(r => string.Equals(r.Email, email, StringComparison.OrdinalIgnoreCase)))
|
|
{
|
|
_receiverPopupValidationMessage = L("SenderEditor_Validation_EmailDuplicate", "Diese E-Mail-Adresse wurde bereits hinzugefügt.");
|
|
return Task.CompletedTask;
|
|
}
|
|
|
|
_receivers.Add(new ReceiverDraft(Guid.NewGuid(), null, fullName, email, phoneNumber,
|
|
ReceiverPalette[_receivers.Count % ReceiverPalette.Length]));
|
|
CloseAddReceiverPopup();
|
|
return Task.CompletedTask;
|
|
}
|
|
|
|
async Task RemoveReceiverAsync(ReceiverDraft receiver)
|
|
{
|
|
if (_isSaving)
|
|
return;
|
|
|
|
var envelopeId = EnvelopeId ?? _loadedEnvelopeId;
|
|
|
|
if (envelopeId is int persistedEnvelopeId && persistedEnvelopeId > 0 && receiver.ReceiverId is int persistedReceiverId && persistedReceiverId > 0)
|
|
{
|
|
try
|
|
{
|
|
await EnvelopeReceiverService.DeleteAsync(persistedEnvelopeId, persistedReceiverId);
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
Logger.LogError(ex, "[SenderEditor] Failed to remove receiver {ReceiverId} from envelope {EnvelopeId}", persistedReceiverId, persistedEnvelopeId);
|
|
_saveErrorMessage = string.Format(
|
|
L("SenderEditor_RemoveReceiverError", "Empfaenger konnte nicht entfernt werden: {0}"),
|
|
ex.Message);
|
|
_savePopupVisible = true;
|
|
return;
|
|
}
|
|
}
|
|
|
|
_receivers.RemoveAll(r => r.Id == receiver.Id);
|
|
_signatureFields.RemoveAll(f => f.ReceiverDraftId == receiver.Id);
|
|
|
|
if (_pendingReceiverForPlacement?.Id == receiver.Id)
|
|
_pendingReceiverForPlacement = null;
|
|
}
|
|
|
|
// ── Models ──
|
|
record SignatureFieldDraft(double XPt, double YPt, int Page, Guid ReceiverDraftId, string ReceiverName, string Color);
|
|
|
|
record NormalisedCoords(double NormX, double NormY, int PageIndex);
|
|
|
|
record ReceiverDraft(Guid Id, int? ReceiverId, string FullName, string Email, string PhoneNumber, string Color);
|
|
|
|
// ── Receiver colour palette (cycles when > 8 receivers) ──
|
|
static readonly string[] ReceiverPalette =
|
|
[
|
|
"#4F46E5", // indigo
|
|
"#059669", // emerald
|
|
"#DC2626", // red
|
|
"#D97706", // amber
|
|
"#7C3AED", // violet
|
|
"#0891B2", // cyan
|
|
"#BE185D", // pink
|
|
"#65A30D", // lime
|
|
];
|
|
|
|
string L(string key, string fallback)
|
|
{
|
|
var modelValue = ModelLocalizer[key];
|
|
if (!modelValue.ResourceNotFound)
|
|
return modelValue.Value;
|
|
|
|
var uiValue = UiLocalizer[key];
|
|
return uiValue.ResourceNotFound ? fallback : uiValue.Value;
|
|
}
|
|
}
|