Compare commits
23 Commits
4d069cdaa0
...
feat/migr-
| Author | SHA1 | Date | |
|---|---|---|---|
| 77379265c2 | |||
| 9b68c09b0b | |||
| cd6bf05352 | |||
| b2c205b160 | |||
| 33439f4a65 | |||
| b5cdf79799 | |||
| 72915ac001 | |||
| e4d7a0d8f3 | |||
| d5f9de06f3 | |||
| bbc129178f | |||
| 40c95100ff | |||
| 1b2731b4b2 | |||
| ae56902758 | |||
| e80059d34e | |||
| a2443032c5 | |||
| 3d2fe532e5 | |||
| 5e83aa26c9 | |||
| 2c789cd4c0 | |||
| 9ecfe08e2e | |||
| 74da6e37b0 | |||
| 10f65e583a | |||
| 0cdc8f1191 | |||
| 56ccab6377 |
4
.gitignore
vendored
4
.gitignore
vendored
@@ -365,3 +365,7 @@ FodyWeavers.xsd
|
|||||||
/EnvelopeGenerator.GeneratorAPI/ClientApp/envelope-generator-ui/.vscode
|
/EnvelopeGenerator.GeneratorAPI/ClientApp/envelope-generator-ui/.vscode
|
||||||
/EnvelopeGenerator.Tests.Application/Services/BugFixTests.cs
|
/EnvelopeGenerator.Tests.Application/Services/BugFixTests.cs
|
||||||
/EnvelopeGenerator.Tests.Application/annotations.json
|
/EnvelopeGenerator.Tests.Application/annotations.json
|
||||||
|
/EnvelopeGenerator.Server/EnvelopeGenerator.Server/TekH - SoftHSM Test.md
|
||||||
|
/EnvelopeGenerator.Server/EnvelopeGenerator.Server/tekh_softHSM_test.md
|
||||||
|
/EnvelopeGenerator.Server/EnvelopeGenerator.Server/publish-output
|
||||||
|
/EnvelopeGenerator.Server/EnvelopeGenerator.Server/tekh_softHSM_test.md
|
||||||
|
|||||||
@@ -214,6 +214,10 @@ public class EnvelopeReceiverController : ControllerBase
|
|||||||
if (reader.Read())
|
if (reader.Read())
|
||||||
{
|
{
|
||||||
bool outSuccess = reader.GetBoolean(0);
|
bool outSuccess = reader.GetBoolean(0);
|
||||||
|
if (!outSuccess)
|
||||||
|
_logger.LogWarning(
|
||||||
|
"PRSIG_API_ADD_DOC_RECEIVER_ELEM returned OUT_SUCCESS=false. DOC_ID={DocId}, RECEIVER_ID={ReceiverId}, Page={Page}",
|
||||||
|
document.Id, rcv.Id, sign.Page);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
#endregion
|
#endregion
|
||||||
@@ -221,8 +225,6 @@ public class EnvelopeReceiverController : ControllerBase
|
|||||||
#region Create history
|
#region Create history
|
||||||
// ENV_UID, STATUS_ID, USER_ID,
|
// ENV_UID, STATUS_ID, USER_ID,
|
||||||
string sql_hist = @"
|
string sql_hist = @"
|
||||||
USE [DD_ECM]
|
|
||||||
|
|
||||||
DECLARE @OUT_SUCCESS bit;
|
DECLARE @OUT_SUCCESS bit;
|
||||||
|
|
||||||
EXEC [dbo].[PRSIG_API_ADD_HISTORY_STATE]
|
EXEC [dbo].[PRSIG_API_ADD_HISTORY_STATE]
|
||||||
@@ -244,6 +246,10 @@ public class EnvelopeReceiverController : ControllerBase
|
|||||||
if (reader.Read())
|
if (reader.Read())
|
||||||
{
|
{
|
||||||
bool outSuccess = reader.GetBoolean(0);
|
bool outSuccess = reader.GetBoolean(0);
|
||||||
|
if (!outSuccess)
|
||||||
|
_logger.LogWarning(
|
||||||
|
"PRSIG_API_ADD_HISTORY_STATE returned OUT_SUCCESS=false. EnvelopeUuid={EnvelopeUuid}",
|
||||||
|
envelope.Uuid);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
#endregion
|
#endregion
|
||||||
|
|||||||
@@ -13,7 +13,7 @@ public class EnvelopeReceiverAddReadSQL : ISQL<Envelope>
|
|||||||
/// ENV_UID, EMAIL_ADRESS, SALUTATION, PHONE,
|
/// ENV_UID, EMAIL_ADRESS, SALUTATION, PHONE,
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public string Raw => @"
|
public string Raw => @"
|
||||||
DECLARE @OUT_RECEIVER_ID int
|
DECLARE @OUT_RECEIVER_ID bigint
|
||||||
|
|
||||||
EXEC [dbo].[PRSIG_API_CREATE_RECEIVER]
|
EXEC [dbo].[PRSIG_API_CREATE_RECEIVER]
|
||||||
{0},
|
{0},
|
||||||
|
|||||||
@@ -138,7 +138,9 @@
|
|||||||
SelectedDataItemChanged="@OnSelectedEnvelopeChanged"
|
SelectedDataItemChanged="@OnSelectedEnvelopeChanged"
|
||||||
CustomizeElement="OnCustomizeElement">
|
CustomizeElement="OnCustomizeElement">
|
||||||
<Columns>
|
<Columns>
|
||||||
<DxGridDataColumn FieldName="Id" Caption="ID">
|
<DxGridDataColumn FieldName="Id" Caption="ID"
|
||||||
|
SortIndex="0"
|
||||||
|
SortOrder="GridColumnSortOrder.Descending">
|
||||||
<CellDisplayTemplate Context="cellContext">
|
<CellDisplayTemplate Context="cellContext">
|
||||||
@((cellContext.DataItem as EnvelopeDto)?.Id)
|
@((cellContext.DataItem as EnvelopeDto)?.Id)
|
||||||
</CellDisplayTemplate>
|
</CellDisplayTemplate>
|
||||||
@@ -234,7 +236,9 @@
|
|||||||
SelectedDataItemChanged="@OnSelectedEnvelopeChanged"
|
SelectedDataItemChanged="@OnSelectedEnvelopeChanged"
|
||||||
CustomizeElement="OnCustomizeElement">
|
CustomizeElement="OnCustomizeElement">
|
||||||
<Columns>
|
<Columns>
|
||||||
<DxGridDataColumn FieldName="Id" Caption="ID">
|
<DxGridDataColumn FieldName="Id" Caption="ID"
|
||||||
|
SortIndex="0"
|
||||||
|
SortOrder="GridColumnSortOrder.Descending">
|
||||||
<CellDisplayTemplate Context="cellContext">
|
<CellDisplayTemplate Context="cellContext">
|
||||||
@((cellContext.DataItem as EnvelopeDto)?.Id)
|
@((cellContext.DataItem as EnvelopeDto)?.Id)
|
||||||
</CellDisplayTemplate>
|
</CellDisplayTemplate>
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
@page "/"
|
@page "/"
|
||||||
@inject IJSRuntime JS
|
@inject IJSRuntime JS
|
||||||
|
@inject NavigationManager Navigation
|
||||||
@rendermode InteractiveWebAssembly
|
@rendermode InteractiveWebAssembly
|
||||||
|
|
||||||
<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" />
|
||||||
@@ -26,7 +27,18 @@
|
|||||||
<span id="home-description"></span>
|
<span id="home-description"></span>
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
<div class="mt-4 pt-3 border-top">
|
<div class="mt-4 pt-3 border-top d-flex flex-column align-items-center gap-4">
|
||||||
|
|
||||||
|
<button class="home-btn-primary btn px-4 py-2 d-flex align-items-center gap-2" @onclick='() => Navigation.NavigateTo("/sender")'>
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" width="18" height="18" fill="currentColor" viewBox="0 0 16 16">
|
||||||
|
<path d="M0 4a2 2 0 0 1 2-2h12a2 2 0 0 1 2 2v8a2 2 0 0 1-2 2H2a2 2 0 0 1-2-2V4Zm2-1a1 1 0 0 0-1 1v.217l7 4.2 7-4.2V4a1 1 0 0 0-1-1H2Zm13 2.383-4.708 2.825L15 11.105V5.383Zm-.034 6.876-5.64-3.471L8 9.583l-1.326-.795-5.64 3.47A1 1 0 0 0 2 13h12a1 1 0 0 0 .966-.741ZM1 11.105l4.708-2.897L1 5.383v5.722Z"/>
|
||||||
|
</svg>
|
||||||
|
Zur Umschlag-Übersicht
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" viewBox="0 0 16 16">
|
||||||
|
<path fill-rule="evenodd" d="M4.646 1.646a.5.5 0 0 1 .708 0l6 6a.5.5 0 0 1 0 .708l-6 6a.5.5 0 0 1-.708-.708L10.293 8 4.646 2.354a.5.5 0 0 1 0-.708z"/>
|
||||||
|
</svg>
|
||||||
|
</button>
|
||||||
|
|
||||||
<div class="d-flex flex-wrap justify-content-center gap-3">
|
<div class="d-flex flex-wrap justify-content-center gap-3">
|
||||||
<div class="home-feature-badge">
|
<div class="home-feature-badge">
|
||||||
<svg xmlns="http://www.w3.org/2000/svg" width="15" height="15" fill="currentColor" class="me-1" viewBox="0 0 16 16">
|
<svg xmlns="http://www.w3.org/2000/svg" width="15" height="15" fill="currentColor" class="me-1" viewBox="0 0 16 16">
|
||||||
@@ -48,6 +60,7 @@
|
|||||||
PDF-Export
|
PDF-Export
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -161,7 +161,7 @@
|
|||||||
|
|
||||||
if (result == EnvelopeLoginResult.Success)
|
if (result == EnvelopeLoginResult.Success)
|
||||||
{
|
{
|
||||||
Navigation.NavigateTo($"/envelope/{Uri.EscapeDataString(EnvelopeKey)}/report", forceLoad: true);
|
Navigation.NavigateTo($"/envelope/{Uri.EscapeDataString(EnvelopeKey)}", forceLoad: true);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ using System.Net;
|
|||||||
using System.Net.Http;
|
using System.Net.Http;
|
||||||
using System.Net.Http.Json;
|
using System.Net.Http.Json;
|
||||||
using System.Text.Json;
|
using System.Text.Json;
|
||||||
|
using EnvelopeGenerator.Application.EnvelopeReceivers.Commands;
|
||||||
using EnvelopeGenerator.Server.Client.Models;
|
using EnvelopeGenerator.Server.Client.Models;
|
||||||
|
|
||||||
namespace EnvelopeGenerator.Server.Client.Services;
|
namespace EnvelopeGenerator.Server.Client.Services;
|
||||||
@@ -9,6 +10,7 @@ namespace EnvelopeGenerator.Server.Client.Services;
|
|||||||
/// <summary>
|
/// <summary>
|
||||||
/// Retrieves the <see cref="EnvelopeReceiverDto"/> for the authenticated receiver
|
/// Retrieves the <see cref="EnvelopeReceiverDto"/> for the authenticated receiver
|
||||||
/// from <c>GET /api/EnvelopeReceiver/{envelopeKey}</c>.
|
/// from <c>GET /api/EnvelopeReceiver/{envelopeKey}</c>.
|
||||||
|
/// Also creates new envelopes via <c>POST /api/EnvelopeReceiver</c>.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public class EnvelopeReceiverService(IHttpClientFactory httpClientFactory)
|
public class EnvelopeReceiverService(IHttpClientFactory httpClientFactory)
|
||||||
{
|
{
|
||||||
@@ -37,4 +39,28 @@ public class EnvelopeReceiverService(IHttpClientFactory httpClientFactory)
|
|||||||
|
|
||||||
return await response.Content.ReadFromJsonAsync<EnvelopeReceiverDto>(_jsonOptions, cancel);
|
return await response.Content.ReadFromJsonAsync<EnvelopeReceiverDto>(_jsonOptions, cancel);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Creates a new envelope with document and receivers via <c>POST /api/EnvelopeReceiver</c>.
|
||||||
|
/// Requires sender authentication cookie to be present in the request.
|
||||||
|
/// </summary>
|
||||||
|
/// <exception cref="HttpRequestException">Thrown when the API request fails.</exception>
|
||||||
|
public async Task<CreateEnvelopeReceiverResponse?> CreateAsync(
|
||||||
|
CreateEnvelopeReceiverCommand request,
|
||||||
|
CancellationToken cancel = default)
|
||||||
|
{
|
||||||
|
using var http = httpClientFactory.CreateClient("EnvelopeGenerator.Server");
|
||||||
|
var response = await http.PostAsJsonAsync("/api/EnvelopeReceiver", request, _jsonOptions, cancel);
|
||||||
|
|
||||||
|
if (!response.IsSuccessStatusCode)
|
||||||
|
{
|
||||||
|
var body = await response.Content.ReadAsStringAsync(cancel);
|
||||||
|
throw new HttpRequestException(
|
||||||
|
$"Fehler beim Erstellen des Umschlags. Status: {(int)response.StatusCode} – {body}",
|
||||||
|
null,
|
||||||
|
response.StatusCode);
|
||||||
|
}
|
||||||
|
|
||||||
|
return await response.Content.ReadFromJsonAsync<CreateEnvelopeReceiverResponse>(_jsonOptions, cancel);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,15 +3,21 @@
|
|||||||
@using DevExpress.Blazor.PdfViewer
|
@using DevExpress.Blazor.PdfViewer
|
||||||
@using DevExpress.Blazor.Reporting.Models
|
@using DevExpress.Blazor.Reporting.Models
|
||||||
@using DevExpress.Blazor
|
@using DevExpress.Blazor
|
||||||
|
@using EnvelopeGenerator.Application.EnvelopeReceivers.Commands
|
||||||
@using EnvelopeGenerator.Server.Client.Services
|
@using EnvelopeGenerator.Server.Client.Services
|
||||||
@using EnvelopeGenerator.Server.Services
|
@using EnvelopeGenerator.Server.Services
|
||||||
@using Microsoft.AspNetCore.Components.Forms
|
@using Microsoft.AspNetCore.Components.Forms
|
||||||
|
@using Microsoft.Extensions.Caching.Memory
|
||||||
@inject IJSRuntime JSRuntime
|
@inject IJSRuntime JSRuntime
|
||||||
|
@inject NavigationManager NavigationManager
|
||||||
@inject AppVersionService AppVersion
|
@inject AppVersionService AppVersion
|
||||||
@inject ILogger<EnvelopeSenderEditorPage> Logger
|
@inject ILogger<EnvelopeSenderEditorPage> Logger
|
||||||
@inject EnvelopeReceiverPageDataService ReceiverPageDataService
|
@inject EnvelopeReceiverPageDataService ReceiverPageDataService
|
||||||
|
@inject EnvelopeReceiverService EnvelopeReceiverService
|
||||||
|
@inject IMemoryCache MemoryCache
|
||||||
|
|
||||||
<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" />
|
||||||
|
<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" />
|
<link href="@AppVersion.GetVersionedUrl("css/envelope-viewer.css")" rel="stylesheet" />
|
||||||
<script src="@AppVersion.GetVersionedUrl("js/envelope-editor.js")"></script>
|
<script src="@AppVersion.GetVersionedUrl("js/envelope-editor.js")"></script>
|
||||||
|
|
||||||
@@ -22,14 +28,55 @@
|
|||||||
<div class="envelope-action-bar__inner"
|
<div class="envelope-action-bar__inner"
|
||||||
style="flex-direction: row; align-items: center; padding: 0.35rem 1.5rem; gap: 0.75rem;">
|
style="flex-direction: row; align-items: center; padding: 0.35rem 1.5rem; gap: 0.75rem;">
|
||||||
|
|
||||||
@* Left: Title *@
|
@* Left: Title + meta inputs + receivers *@
|
||||||
<div style="flex: 1; min-width: 0; display: flex; flex-direction: column; align-items: stretch; gap: 0.75rem;">
|
<div style="flex: 1; min-width: 0; display: flex; flex-direction: column; align-items: stretch; gap: 0.5rem;">
|
||||||
<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;">
|
@* ── Title & Message inputs ── *@
|
||||||
Neues Dokument
|
<div style="display: flex; align-items: flex-start; gap: 0.5rem; flex-wrap: wrap;">
|
||||||
|
|
||||||
|
@* Titel *@
|
||||||
|
<div style="display: flex; flex-direction: column; gap: 0.2rem; flex: 1 1 180px; min-width: 140px;">
|
||||||
|
<label style="font-size: 0.65rem; font-weight: 600; color: #6b7280;
|
||||||
|
text-transform: uppercase; letter-spacing: 0.05em;">
|
||||||
|
Titel <span style="color:#dc2626;">*</span>
|
||||||
|
</label>
|
||||||
|
<input type="text"
|
||||||
|
value="@_envelopeTitle"
|
||||||
|
@oninput="e => _envelopeTitle = e.Value?.ToString() ?? string.Empty"
|
||||||
|
placeholder="z.B. Arbeitsvertrag 2025"
|
||||||
|
maxlength="120"
|
||||||
|
style="font-size: 0.78rem; font-weight: 500; color: #1f2937;
|
||||||
|
border: 1px solid #d1d5db; border-radius: 5px;
|
||||||
|
padding: 0.3rem 0.55rem; outline: none; width: 100%;
|
||||||
|
background: #fff;
|
||||||
|
@(_titleTouched && string.IsNullOrWhiteSpace(_envelopeTitle) ? "border-color:#dc2626;" : "")" />
|
||||||
|
@if (_titleTouched && string.IsNullOrWhiteSpace(_envelopeTitle))
|
||||||
|
{
|
||||||
|
<span style="font-size:0.6rem;color:#dc2626;">Pflichtfeld</span>
|
||||||
|
}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
@* Nachricht *@
|
||||||
|
<div style="display: flex; flex-direction: column; gap: 0.2rem; flex: 2 1 260px; min-width: 180px;">
|
||||||
|
<label style="font-size: 0.65rem; font-weight: 600; color: #6b7280;
|
||||||
|
text-transform: uppercase; letter-spacing: 0.05em;">
|
||||||
|
Nachricht <span style="color:#6b7280;font-weight:400;">(optional)</span>
|
||||||
|
</label>
|
||||||
|
<input type="text"
|
||||||
|
value="@_envelopeMessage"
|
||||||
|
@oninput="e => _envelopeMessage = e.Value?.ToString() ?? string.Empty"
|
||||||
|
placeholder="Bitte unterzeichnen Sie das beigefügte Dokument."
|
||||||
|
maxlength="500"
|
||||||
|
style="font-size: 0.78rem; color: #1f2937;
|
||||||
|
border: 1px solid #d1d5db; border-radius: 5px;
|
||||||
|
padding: 0.3rem 0.55rem; outline: none; width: 100%;
|
||||||
|
background: #fff;" />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
@* PDF filename badge *@
|
||||||
@if (_pdfLoaded)
|
@if (_pdfLoaded)
|
||||||
{
|
{
|
||||||
|
<div style="display:flex;align-items:flex-end;gap:0.4rem;padding-bottom:0.05rem;flex-shrink:0;">
|
||||||
<span style="font-size: 0.7rem; color: #6b7280;">@_fileName</span>
|
<span style="font-size: 0.7rem; color: #6b7280;">@_fileName</span>
|
||||||
@if (_signatureFields.Count > 0)
|
@if (_signatureFields.Count > 0)
|
||||||
{
|
{
|
||||||
@@ -39,6 +86,7 @@
|
|||||||
@_signatureFields.Count Signaturfeld@(_signatureFields.Count != 1 ? "er" : "")
|
@_signatureFields.Count Signaturfeld@(_signatureFields.Count != 1 ? "er" : "")
|
||||||
</span>
|
</span>
|
||||||
}
|
}
|
||||||
|
</div>
|
||||||
}
|
}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -70,7 +118,8 @@
|
|||||||
<div class="sender-receivers-list">
|
<div class="sender-receivers-list">
|
||||||
@foreach (var receiver in _receivers)
|
@foreach (var receiver in _receivers)
|
||||||
{
|
{
|
||||||
<div class="sender-receiver-chip">
|
<div class="sender-receiver-chip"
|
||||||
|
style="border-left: 3px solid @receiver.Color;">
|
||||||
<div class="sender-receiver-chip__body">
|
<div class="sender-receiver-chip__body">
|
||||||
<div class="sender-receiver-chip__name">@receiver.FullName</div>
|
<div class="sender-receiver-chip__name">@receiver.FullName</div>
|
||||||
<div class="sender-receiver-chip__email">@receiver.Email</div>
|
<div class="sender-receiver-chip__email">@receiver.Email</div>
|
||||||
@@ -80,9 +129,11 @@
|
|||||||
}
|
}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<button class="pdf-toolbar__btn pdf-toolbar__btn--signature-change sender-toolbar-action-btn sender-toolbar-action-btn--compact"
|
<button class="pdf-toolbar__btn pdf-toolbar__btn--signature-change sender-toolbar-action-btn sender-toolbar-action-btn--compact
|
||||||
@onclick="() => AddSignatureForReceiver(receiver)"
|
@(_pendingReceiverForPlacement?.Id == receiver.Id ? "pdf-toolbar__btn--signature-change-active" : "")"
|
||||||
title="Signatur hinzufügen">
|
@onclick="() => ActivatePlacementForReceiver(receiver)"
|
||||||
|
style="@(_pendingReceiverForPlacement?.Id == receiver.Id ? $"background:{receiver.Color};color:#fff;border-color:{receiver.Color};" : $"color:{receiver.Color};border-color:{receiver.Color};")"
|
||||||
|
title="Signaturfeld platzieren">
|
||||||
<svg xmlns="http://www.w3.org/2000/svg" width="14" height="14" fill="currentColor" viewBox="0 0 16 16">
|
<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" />
|
<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>
|
</svg>
|
||||||
@@ -98,6 +149,12 @@
|
|||||||
@* Right: Buttons *@
|
@* Right: Buttons *@
|
||||||
<div style="display: flex; align-items: center; gap: 0.5rem; flex-shrink: 0;">
|
<div style="display: flex; align-items: center; gap: 0.5rem; flex-shrink: 0;">
|
||||||
|
|
||||||
|
<button class="pdf-toolbar__btn pdf-toolbar__btn--reset"
|
||||||
|
@onclick="Cancel"
|
||||||
|
title="Abbrechen">
|
||||||
|
Abbrechen
|
||||||
|
</button>
|
||||||
|
|
||||||
@* PDF Upload *@
|
@* PDF Upload *@
|
||||||
<label class="pdf-toolbar__btn"
|
<label class="pdf-toolbar__btn"
|
||||||
title="PDF hochladen"
|
title="PDF hochladen"
|
||||||
@@ -114,23 +171,11 @@
|
|||||||
|
|
||||||
@if (_pdfLoaded)
|
@if (_pdfLoaded)
|
||||||
{
|
{
|
||||||
@* Toggle placement mode *@
|
|
||||||
<button class="pdf-toolbar__btn @(_placementMode ? "pdf-toolbar__btn--signature-change-active" : "pdf-toolbar__btn--signature-change")"
|
|
||||||
@onclick="TogglePlacementMode"
|
|
||||||
title="@(_placementMode ? "Platzierungsmodus beenden" : "Signaturfeld hinzufügen")">
|
|
||||||
<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>
|
|
||||||
<span class="pdf-toolbar__btn-text">
|
|
||||||
@(_placementMode ? "Abbrechen" : "+ Signaturfeld")
|
|
||||||
</span>
|
|
||||||
</button>
|
|
||||||
|
|
||||||
@* Clear all fields *@
|
@* Clear all fields *@
|
||||||
@if (_signatureFields.Count > 0)
|
@if (_signatureFields.Count > 0)
|
||||||
{
|
{
|
||||||
<button class="pdf-toolbar__btn pdf-toolbar__btn--reset"
|
<button class="pdf-toolbar__btn pdf-toolbar__btn--reset"
|
||||||
@onclick="ClearAllFields"
|
@onclick="ClearAllFieldsAsync"
|
||||||
title="Alle Signaturfelder entfernen">
|
title="Alle Signaturfelder entfernen">
|
||||||
<svg xmlns="http://www.w3.org/2000/svg" width="14" height="14" fill="currentColor" viewBox="0 0 16 16">
|
<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 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" />
|
||||||
@@ -142,11 +187,20 @@
|
|||||||
@* Save *@
|
@* Save *@
|
||||||
<button class="pdf-toolbar__btn pdf-toolbar__btn--success"
|
<button class="pdf-toolbar__btn pdf-toolbar__btn--success"
|
||||||
@onclick="SaveAsync"
|
@onclick="SaveAsync"
|
||||||
|
disabled="@_isSaving"
|
||||||
title="Speichern"
|
title="Speichern"
|
||||||
style="background: linear-gradient(135deg,#059669,#047857); color:#fff; border-radius:6px; padding:0.3rem 0.75rem; font-size:0.75rem; font-weight:600; border:none;">
|
style="background: linear-gradient(135deg,#059669,#047857); color:#fff; border-radius:6px; padding:0.3rem 0.75rem; font-size:0.75rem; font-weight:600; border:none;">
|
||||||
|
@if (_isSaving)
|
||||||
|
{
|
||||||
|
<span class="spinner-border spinner-border-sm me-1" role="status"
|
||||||
|
style="width:0.8rem;height:0.8rem;border-width:2px;"></span>
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
<svg xmlns="http://www.w3.org/2000/svg" width="14" height="14" fill="currentColor" class="me-1" viewBox="0 0 16 16">
|
<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" />
|
<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>
|
</svg>
|
||||||
|
}
|
||||||
Speichern
|
Speichern
|
||||||
</button>
|
</button>
|
||||||
}
|
}
|
||||||
@@ -154,11 +208,15 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
@* Placement mode hint bar *@
|
@* Placement mode hint bar *@
|
||||||
@if (_placementMode)
|
@if (_pendingReceiverForPlacement is not null)
|
||||||
{
|
{
|
||||||
<div style="background: #4F46E5; color: white; font-size: 0.75rem; font-weight: 500;
|
<div style="background: @_pendingReceiverForPlacement.Color; color: white; font-size: 0.75rem; font-weight: 500;
|
||||||
padding: 0.3rem 1.5rem; text-align: center; letter-spacing: 0.01em;">
|
padding: 0.3rem 1.5rem; text-align: center; letter-spacing: 0.01em;">
|
||||||
📌 Klicken Sie auf die gewünschte Stelle im Dokument, um ein Signaturfeld zu platzieren.
|
📌 Klicken Sie auf die Stelle im Dokument für <strong>@_pendingReceiverForPlacement.FullName</strong>.
|
||||||
|
<button @onclick="CancelPlacement"
|
||||||
|
style="background:none;border:none;color:white;cursor:pointer;font-size:0.75rem;text-decoration:underline;padding:0;">
|
||||||
|
Abbrechen
|
||||||
|
</button>
|
||||||
</div>
|
</div>
|
||||||
}
|
}
|
||||||
</div>
|
</div>
|
||||||
@@ -202,63 +260,13 @@
|
|||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
@* PDF viewer + overlay wrapper *@
|
@* PDF viewer — click capture active only in placement mode *@
|
||||||
<div id="pdf-editor-wrapper" class="pdf-editor-wrapper"
|
<div class="pdf-editor-wrapper"
|
||||||
style="position: relative; width: 100%; height: 100%; overflow: auto;">
|
style="cursor: @(_pendingReceiverForPlacement is not null ? "crosshair" : "default");"
|
||||||
|
@onclick="OnPdfAreaClickAsync">
|
||||||
@* DxPdfViewer — zoom fixed to 1.0 for reliable coordinate mapping *@
|
|
||||||
<DxPdfViewer @ref="_pdfViewer"
|
<DxPdfViewer @ref="_pdfViewer"
|
||||||
DocumentContent="@_pdfBytes"
|
DocumentContent="@_pdfBytes"
|
||||||
ZoomLevel="1.0"
|
|
||||||
IsSinglePagePreview="false"
|
|
||||||
CssClass="sender-editor-pdf-viewer" />
|
CssClass="sender-editor-pdf-viewer" />
|
||||||
|
|
||||||
@* Transparent overlay for click capture (active only in placement mode) *@
|
|
||||||
<div id="pdf-editor-overlay"
|
|
||||||
style="position: absolute; top: 0; left: 0; width: 100%; height: 100%;
|
|
||||||
z-index: 20;
|
|
||||||
pointer-events: @(_placementMode ? "auto" : "none");
|
|
||||||
cursor: @(_placementMode ? "crosshair" : "default");"
|
|
||||||
@onclick="OnOverlayClickAsync">
|
|
||||||
|
|
||||||
@* Render placed signature field placeholders *@
|
|
||||||
@foreach (var field in _signatureFields)
|
|
||||||
{
|
|
||||||
var f = field; // capture for lambda
|
|
||||||
<div style="position: absolute;
|
|
||||||
left: @(f.DisplayX.ToString("F1", System.Globalization.CultureInfo.InvariantCulture))px;
|
|
||||||
top: @(f.DisplayY.ToString("F1", System.Globalization.CultureInfo.InvariantCulture))px;
|
|
||||||
width: @(SigDisplayW)px;
|
|
||||||
height: @(SigDisplayH)px;
|
|
||||||
border: 2px dashed #4F46E5;
|
|
||||||
background: rgba(79,70,229,0.10);
|
|
||||||
border-radius: 4px;
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: center;
|
|
||||||
gap: 2px;
|
|
||||||
pointer-events: auto;
|
|
||||||
cursor: default;"
|
|
||||||
@onclick:stopPropagation="true">
|
|
||||||
<span style="font-size: 0.6rem; font-weight: 700; color: #4F46E5;
|
|
||||||
letter-spacing: 0.05em; text-transform: uppercase;">
|
|
||||||
Unterschrift
|
|
||||||
</span>
|
|
||||||
<span style="font-size: 0.55rem; color: #6d28d9; opacity: 0.8;">
|
|
||||||
S.@f.Page
|
|
||||||
</span>
|
|
||||||
<button @onclick="() => RemoveField(f)"
|
|
||||||
style="position: absolute; top: 2px; right: 4px;
|
|
||||||
background: none; border: none; cursor: pointer;
|
|
||||||
color: #6d28d9; font-size: 0.75rem; line-height: 1;
|
|
||||||
padding: 0; opacity: 0.7;"
|
|
||||||
title="Entfernen">
|
|
||||||
×
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
}
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
}
|
}
|
||||||
</div>
|
</div>
|
||||||
@@ -312,7 +320,7 @@
|
|||||||
TValue="string"
|
TValue="string"
|
||||||
Data="@_receiverEmailSuggestions"
|
Data="@_receiverEmailSuggestions"
|
||||||
Value="@_selectedReceiverEmailSuggestion"
|
Value="@_selectedReceiverEmailSuggestion"
|
||||||
ValueChanged="OnReceiverEmailSuggestionSelectedAsync"
|
ValueChanged="OnReceiverEmailSuggestionCommittedAsync"
|
||||||
SelectionMode="ListBoxSelectionMode.Single"
|
SelectionMode="ListBoxSelectionMode.Single"
|
||||||
CssClass="w-100" />
|
CssClass="w-100" />
|
||||||
</div>
|
</div>
|
||||||
@@ -348,25 +356,116 @@
|
|||||||
</FooterContentTemplate>
|
</FooterContentTemplate>
|
||||||
</DxPopup>
|
</DxPopup>
|
||||||
|
|
||||||
|
@* ── Save result popup (success + error) ── *@
|
||||||
|
<DxPopup @bind-Visible="_savePopupVisible"
|
||||||
|
HeaderText="@(_saveErrorMessage is null ? "Umschlag erstellt" : "Fehler")"
|
||||||
|
Width="460px"
|
||||||
|
MaxWidth="95vw"
|
||||||
|
ShowFooter="true"
|
||||||
|
CloseOnOutsideClick="false"
|
||||||
|
ShowCloseButton="false"
|
||||||
|
CloseOnEscape="false">
|
||||||
|
<BodyContentTemplate>
|
||||||
|
@if (_saveErrorMessage is null)
|
||||||
|
{
|
||||||
|
<div style="display:flex;align-items:flex-start;gap:1rem;padding:0.5rem 0;">
|
||||||
|
<div style="flex-shrink:0;width:40px;height:40px;background:#d1fae5;border-radius:50%;
|
||||||
|
display:flex;align-items:center;justify-content:center;">
|
||||||
|
<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 style="margin:0 0 0.4rem;font-weight:600;color:#1f2937;font-size:0.95rem;">
|
||||||
|
Umschlag wurde erfolgreich erstellt.
|
||||||
|
</p>
|
||||||
|
<p style="margin:0;color:#6b7280;font-size:0.85rem;line-height:1.5;">
|
||||||
|
Der Umschlag wurde gespeichert und die Empfänger wurden benachrichtigt.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
<div style="display:flex;align-items:flex-start;gap:1rem;padding:0.5rem 0;">
|
||||||
|
<div style="flex-shrink:0;width:40px;height:40px;background:#fee2e2;border-radius:50%;
|
||||||
|
display:flex;align-items:center;justify-content:center;">
|
||||||
|
<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 style="margin:0 0 0.4rem;font-weight:600;color:#1f2937;font-size:0.95rem;">
|
||||||
|
Fehler beim Erstellen des Umschlags
|
||||||
|
</p>
|
||||||
|
<p style="margin:0;color:#6b7280;font-size:0.85rem;line-height:1.5;word-break:break-word;">
|
||||||
|
@_saveErrorMessage
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
</BodyContentTemplate>
|
||||||
|
<FooterContentTemplate>
|
||||||
|
<div style="display:flex;gap:0.5rem;justify-content:flex-end;width:100%;padding:0.5rem 0;">
|
||||||
|
@if (_saveErrorMessage is null)
|
||||||
|
{
|
||||||
|
<button class="btn btn-primary"
|
||||||
|
@onclick="GoToDashboard"
|
||||||
|
style="background:linear-gradient(135deg,#059669,#047857);border:none;border-radius:6px;
|
||||||
|
padding:0.5rem 1.5rem;font-weight:600;color:#fff;">
|
||||||
|
Zur Übersicht
|
||||||
|
</button>
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
<button class="btn btn-outline-secondary"
|
||||||
|
@onclick="() => _savePopupVisible = false"
|
||||||
|
style="border-radius:6px;padding:0.5rem 1.25rem;font-weight:500;">
|
||||||
|
Schließen
|
||||||
|
</button>
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
</FooterContentTemplate>
|
||||||
|
</DxPopup>
|
||||||
|
|
||||||
@code {
|
@code {
|
||||||
|
// ── Session query param — persists across SignalR reconnects ──
|
||||||
|
[SupplyParameterFromQuery(Name = "esid")]
|
||||||
|
public string? Esid { get; set; }
|
||||||
|
|
||||||
// ── Constants ──
|
// ── Constants ──
|
||||||
// Signature field size in PDF points (fixed): 1.77" × 1.96" × 72 pt/inch
|
// Signature field size in PDF points (fixed): 1.77" × 1.96"
|
||||||
const double SigWidthPt = 1.77 * 72; // 127.44 pt
|
const double SigWidthPt = 1.77 * 72; // 127.44 pt
|
||||||
const double SigHeightPt = 1.96 * 72; // 141.12 pt
|
const double SigHeightPt = 1.96 * 72; // 141.12 pt
|
||||||
|
|
||||||
// Display size of the overlay placeholder (pixels at zoom=1.0).
|
// CssClass for DxPdfViewer — used by JS to locate page elements
|
||||||
// At zoom=1.0, 1 CSS px ≈ 1 pt in the DxPdfViewer render.
|
const string ViewerCssClass = "sender-editor-pdf-viewer";
|
||||||
const double SigDisplayW = SigWidthPt;
|
|
||||||
const double SigDisplayH = SigHeightPt;
|
// Cache TTL for editor session (30 min of inactivity)
|
||||||
|
static readonly TimeSpan SessionTtl = TimeSpan.FromMinutes(30);
|
||||||
|
|
||||||
// ── State ──
|
// ── State ──
|
||||||
DxPdfViewer? _pdfViewer;
|
DxPdfViewer? _pdfViewer;
|
||||||
byte[]? _pdfBytes;
|
|
||||||
bool _pdfLoaded = false;
|
bool _pdfLoaded = false;
|
||||||
string _fileName = string.Empty;
|
string _fileName = string.Empty;
|
||||||
string? _errorMessage;
|
string? _errorMessage;
|
||||||
bool _placementMode = false;
|
byte[]? _pdfBytes; // Current rendered PDF (original + placeholders burned in)
|
||||||
|
byte[]? _originalPdfBytes; // Pristine upload — never modified, used as base for redraw
|
||||||
|
|
||||||
List<SignatureFieldDraft> _signatureFields = [];
|
List<SignatureFieldDraft> _signatureFields = [];
|
||||||
|
ReceiverDraft? _pendingReceiverForPlacement; // Set when user clicks "Signatur hinzufügen"
|
||||||
|
|
||||||
|
// ── Save state ──
|
||||||
|
bool _isSaving = false;
|
||||||
|
bool _savePopupVisible = false;
|
||||||
|
string? _saveErrorMessage = null;
|
||||||
|
|
||||||
|
// ── Envelope metadata ──
|
||||||
|
string _envelopeTitle = string.Empty;
|
||||||
|
string _envelopeMessage = string.Empty;
|
||||||
|
bool _titleTouched = false;
|
||||||
|
|
||||||
List<ReceiverDraft> _receivers = [];
|
List<ReceiverDraft> _receivers = [];
|
||||||
bool _receiverPopupVisible;
|
bool _receiverPopupVisible;
|
||||||
string _receiverDraftName = string.Empty;
|
string _receiverDraftName = string.Empty;
|
||||||
@@ -377,8 +476,55 @@
|
|||||||
bool _isReceiverEmailSearchRunning;
|
bool _isReceiverEmailSearchRunning;
|
||||||
List<string> _receiverEmailSuggestions = [];
|
List<string> _receiverEmailSuggestions = [];
|
||||||
int _receiverEmailSearchVersion;
|
int _receiverEmailSearchVersion;
|
||||||
|
|
||||||
static readonly System.ComponentModel.DataAnnotations.EmailAddressAttribute ReceiverEmailValidator = new();
|
static readonly System.ComponentModel.DataAnnotations.EmailAddressAttribute ReceiverEmailValidator = new();
|
||||||
|
|
||||||
|
// ── Cache key helper ──
|
||||||
|
string SessionKey => $"sender-editor:{Esid}";
|
||||||
|
|
||||||
|
// ── Lifecycle ──
|
||||||
|
|
||||||
|
protected override void OnInitialized()
|
||||||
|
{
|
||||||
|
// Intentionally empty.
|
||||||
|
// esid redirect is done in OnAfterRenderAsync to avoid NavigationException
|
||||||
|
// during SSR prerendering, where NavigateTo throws by design.
|
||||||
|
}
|
||||||
|
|
||||||
|
protected override async Task OnAfterRenderAsync(bool firstRender)
|
||||||
|
{
|
||||||
|
if (!firstRender) return;
|
||||||
|
|
||||||
|
// Generate a session id on first interactive render and push it into the URL.
|
||||||
|
// At this point SignalR is connected and NavigateTo is safe.
|
||||||
|
if (string.IsNullOrWhiteSpace(Esid))
|
||||||
|
{
|
||||||
|
var sid = Guid.NewGuid().ToString("N");
|
||||||
|
NavigationManager.NavigateTo($"/sender/editor?esid={sid}", forceLoad: false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
protected override void OnParametersSet()
|
||||||
|
{
|
||||||
|
// After the esid is set via query param, try to restore from cache.
|
||||||
|
if (!string.IsNullOrWhiteSpace(Esid)
|
||||||
|
&& MemoryCache.TryGetValue(SessionKey, out EditorSessionData? cached)
|
||||||
|
&& cached is not null)
|
||||||
|
{
|
||||||
|
_originalPdfBytes = cached.OriginalPdfBytes;
|
||||||
|
_signatureFields = cached.Fields;
|
||||||
|
_fileName = cached.FileName;
|
||||||
|
_pdfLoaded = _originalPdfBytes is { Length: > 0 };
|
||||||
|
_receivers = cached.Receivers;
|
||||||
|
_envelopeTitle = cached.Title;
|
||||||
|
_envelopeMessage = cached.Message;
|
||||||
|
|
||||||
|
// Redraw placeholders onto the original PDF
|
||||||
|
if (_pdfLoaded)
|
||||||
|
_pdfBytes = DrawPlaceholders(_originalPdfBytes!, _signatureFields);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// ── PDF upload ──
|
// ── PDF upload ──
|
||||||
async Task OnPdfFileSelectedAsync(InputFileChangeEventArgs e)
|
async Task OnPdfFileSelectedAsync(InputFileChangeEventArgs e)
|
||||||
{
|
{
|
||||||
@@ -394,17 +540,22 @@
|
|||||||
|
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
// Max 50 MB
|
|
||||||
const long maxBytes = 50 * 1024 * 1024;
|
const long maxBytes = 50 * 1024 * 1024;
|
||||||
using var ms = new System.IO.MemoryStream();
|
using var ms = new System.IO.MemoryStream();
|
||||||
await file.OpenReadStream(maxBytes).CopyToAsync(ms);
|
await file.OpenReadStream(maxBytes).CopyToAsync(ms);
|
||||||
_pdfBytes = ms.ToArray();
|
|
||||||
|
_originalPdfBytes = ms.ToArray();
|
||||||
_fileName = file.Name;
|
_fileName = file.Name;
|
||||||
_pdfLoaded = true;
|
_pdfLoaded = true;
|
||||||
_signatureFields.Clear();
|
_signatureFields.Clear();
|
||||||
_placementMode = false;
|
_pendingReceiverForPlacement = null;
|
||||||
|
|
||||||
Logger.LogInformation("PDF loaded: {Name} ({Size} bytes)", _fileName, _pdfBytes.Length);
|
// Rendered PDF starts as the clean original (no placeholders yet)
|
||||||
|
_pdfBytes = _originalPdfBytes;
|
||||||
|
|
||||||
|
PersistSession();
|
||||||
|
|
||||||
|
Logger.LogInformation("PDF loaded: {Name} ({Size} bytes)", _fileName, _originalPdfBytes.Length);
|
||||||
}
|
}
|
||||||
catch (Exception ex)
|
catch (Exception ex)
|
||||||
{
|
{
|
||||||
@@ -414,79 +565,300 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
// ── Placement mode ──
|
// ── Placement mode ──
|
||||||
void TogglePlacementMode() => _placementMode = !_placementMode;
|
void ActivatePlacementForReceiver(ReceiverDraft receiver)
|
||||||
|
|
||||||
void ClearAllFields()
|
|
||||||
{
|
{
|
||||||
_signatureFields.Clear();
|
// Toggle: clicking the same receiver again cancels placement
|
||||||
_placementMode = false;
|
_pendingReceiverForPlacement = _pendingReceiverForPlacement?.Id == receiver.Id
|
||||||
|
? null
|
||||||
|
: receiver;
|
||||||
}
|
}
|
||||||
|
|
||||||
void RemoveField(SignatureFieldDraft field) => _signatureFields.Remove(field);
|
void CancelPlacement() => _pendingReceiverForPlacement = null;
|
||||||
|
|
||||||
// ── Overlay click → add signature field ──
|
// ── PDF area click → place field ──
|
||||||
async Task OnOverlayClickAsync(MouseEventArgs e)
|
async Task OnPdfAreaClickAsync(MouseEventArgs e)
|
||||||
{
|
{
|
||||||
if (!_placementMode) return;
|
if (_pendingReceiverForPlacement is null) return;
|
||||||
|
if (!_pdfLoaded || _originalPdfBytes is null) return;
|
||||||
|
|
||||||
// Get overlay container bounds via JS
|
// Ask JS for the normalised click position within the rendered PDF page
|
||||||
var coords = await JSRuntime.InvokeAsync<OverlayCoords>(
|
var coords = await JSRuntime.InvokeAsync<NormalisedCoords?>(
|
||||||
"envelopeEditor.getClickCoords", "pdf-editor-overlay",
|
"envelopeEditor.getClickCoordsOnPdfPage",
|
||||||
e.ClientX, e.ClientY);
|
ViewerCssClass, e.ClientX, e.ClientY);
|
||||||
|
|
||||||
if (coords is null) return;
|
if (coords is null)
|
||||||
|
|
||||||
// At zoom=1.0: container pixels ≈ PDF display pixels.
|
|
||||||
// DxPdfViewer renders at 96 dpi by default; PDF points = 72 dpi.
|
|
||||||
// Scale factor: 96/72 = 1.333 → px / 1.333 = pt
|
|
||||||
const double pxToPt = 72.0 / 96.0;
|
|
||||||
|
|
||||||
double xPt = coords.RelX * pxToPt;
|
|
||||||
double yPt = coords.RelY * pxToPt;
|
|
||||||
|
|
||||||
// Active page: DxPdfViewer.ActivePageIndex is 0-based
|
|
||||||
int page = (_pdfViewer?.ActivePageIndex ?? 0) + 1;
|
|
||||||
|
|
||||||
// Display position (px on overlay) — keep in px for CSS
|
|
||||||
double displayX = coords.RelX;
|
|
||||||
double displayY = coords.RelY;
|
|
||||||
|
|
||||||
// Prevent placing outside bounds
|
|
||||||
if (displayX < 0 || displayY < 0) return;
|
|
||||||
if (displayX + SigDisplayW > coords.ContainerW) displayX = coords.ContainerW - SigDisplayW;
|
|
||||||
if (displayY + SigDisplayH > coords.ContainerH) displayY = coords.ContainerH - SigDisplayH;
|
|
||||||
|
|
||||||
var field = new SignatureFieldDraft(xPt, yPt, page, displayX, displayY);
|
|
||||||
_signatureFields.Add(field);
|
|
||||||
|
|
||||||
Logger.LogInformation(
|
|
||||||
"Signature field added: Page={Page} X={X:F1}pt Y={Y:F1}pt",
|
|
||||||
page, xPt, yPt);
|
|
||||||
|
|
||||||
// Exit placement mode after one click (user can re-click button for next)
|
|
||||||
_placementMode = false;
|
|
||||||
}
|
|
||||||
|
|
||||||
// ── Save ──
|
|
||||||
async Task SaveAsync()
|
|
||||||
{
|
{
|
||||||
if (_signatureFields.Count == 0)
|
Logger.LogWarning("[SenderEditor] getClickCoordsOnPdfPage returned null");
|
||||||
{
|
|
||||||
await JSRuntime.InvokeVoidAsync("console.log",
|
|
||||||
"[SenderEditor] No signature fields to save.");
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
foreach (var f in _signatureFields)
|
// Read page dimensions from the original PDF via PdfSharp
|
||||||
|
double pageWidthPt;
|
||||||
|
double pageHeightPt;
|
||||||
|
try
|
||||||
{
|
{
|
||||||
await JSRuntime.InvokeVoidAsync("console.log",
|
using var ms = new System.IO.MemoryStream(_originalPdfBytes);
|
||||||
$"[SenderEditor] Field: Page={f.Page} X={f.XPt:F2}pt ({f.XPt/72:F3}in) Y={f.YPt:F2}pt ({f.YPt/72:F3}in)");
|
var doc = PdfSharp.Pdf.IO.PdfReader.Open(ms, PdfSharp.Pdf.IO.PdfDocumentOpenMode.Import);
|
||||||
|
int pageIndex = Math.Max(0, Math.Min(coords.PageIndex, doc.PageCount - 1));
|
||||||
|
var page = doc.Pages[pageIndex];
|
||||||
|
pageWidthPt = page.Width.Point;
|
||||||
|
pageHeightPt = page.Height.Point;
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
Logger.LogError(ex, "[SenderEditor] Failed to read page dimensions from PDF");
|
||||||
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
await JSRuntime.InvokeVoidAsync("console.log",
|
// Convert normalised [0,1] → PDF points; clamp so box stays inside page
|
||||||
$"[SenderEditor] Total fields: {_signatureFields.Count}");
|
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));
|
||||||
|
|
||||||
|
int page1Based = coords.PageIndex + 1;
|
||||||
|
|
||||||
|
var field = new SignatureFieldDraft(
|
||||||
|
XPt: xPt,
|
||||||
|
YPt: yPt,
|
||||||
|
Page: page1Based,
|
||||||
|
ReceiverName: _pendingReceiverForPlacement.FullName,
|
||||||
|
Color: _pendingReceiverForPlacement.Color);
|
||||||
|
|
||||||
|
_signatureFields.Add(field);
|
||||||
|
_pendingReceiverForPlacement = null;
|
||||||
|
|
||||||
|
// Burn all placeholders onto the original PDF and update the viewer
|
||||||
|
_pdfBytes = DrawPlaceholders(_originalPdfBytes, _signatureFields);
|
||||||
|
PersistSession();
|
||||||
|
|
||||||
|
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 RemoveFieldAsync(SignatureFieldDraft field)
|
||||||
|
{
|
||||||
|
_signatureFields.Remove(field);
|
||||||
|
_pdfBytes = _originalPdfBytes is not null
|
||||||
|
? DrawPlaceholders(_originalPdfBytes, _signatureFields)
|
||||||
|
: _pdfBytes;
|
||||||
|
PersistSession();
|
||||||
|
await Task.CompletedTask;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Clear all fields ──
|
||||||
|
async Task ClearAllFieldsAsync()
|
||||||
|
{
|
||||||
|
_signatureFields.Clear();
|
||||||
|
_pendingReceiverForPlacement = null;
|
||||||
|
_pdfBytes = _originalPdfBytes;
|
||||||
|
PersistSession();
|
||||||
|
await Task.CompletedTask;
|
||||||
|
}
|
||||||
|
|
||||||
|
void Cancel() => NavigationManager.NavigateTo("/sender");
|
||||||
|
|
||||||
|
void GoToDashboard()
|
||||||
|
{
|
||||||
|
// Clear session from cache on successful save so it is not restored if user returns
|
||||||
|
if (!string.IsNullOrWhiteSpace(Esid))
|
||||||
|
MemoryCache.Remove(SessionKey);
|
||||||
|
|
||||||
|
NavigationManager.NavigateTo("/sender");
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Save — POST /api/EnvelopeReceiver ──
|
||||||
|
async Task SaveAsync()
|
||||||
|
{
|
||||||
|
// ── Validation ──
|
||||||
|
_titleTouched = true;
|
||||||
|
if (!_pdfLoaded || _originalPdfBytes is null)
|
||||||
|
{
|
||||||
|
_saveErrorMessage = "Bitte laden Sie zuerst ein PDF-Dokument hoch.";
|
||||||
|
_savePopupVisible = true;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (string.IsNullOrWhiteSpace(_envelopeTitle))
|
||||||
|
{
|
||||||
|
_saveErrorMessage = "Bitte geben Sie einen Titel ein.";
|
||||||
|
_savePopupVisible = true;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (_receivers.Count == 0)
|
||||||
|
{
|
||||||
|
_saveErrorMessage = "Bitte fügen Sie mindestens einen Empfänger hinzu.";
|
||||||
|
_savePopupVisible = true;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (_signatureFields.Count == 0)
|
||||||
|
{
|
||||||
|
_saveErrorMessage = "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.ReceiverName == r.FullName))
|
||||||
|
.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
|
||||||
|
{
|
||||||
|
// ── Build request ──
|
||||||
|
// Document: use the ORIGINAL pdf (without placeholder burn-in) as base64
|
||||||
|
var docBase64 = Convert.ToBase64String(_originalPdfBytes);
|
||||||
|
|
||||||
|
// Build receivers list — each receiver gets their own signature fields
|
||||||
|
// Coordinate conversion: PDF points → inches (DB stores inches)
|
||||||
|
var receiversCmd = _receivers.Select(receiver =>
|
||||||
|
{
|
||||||
|
var fields = _signatureFields
|
||||||
|
.Where(f => f.ReceiverName == receiver.FullName)
|
||||||
|
.Select(f => new DocReceiverElementCreateDto(
|
||||||
|
X: f.XPt / 72.0,
|
||||||
|
Y: f.YPt / 72.0,
|
||||||
|
Page: f.Page))
|
||||||
|
.ToList();
|
||||||
|
|
||||||
|
return new ReceiverGetOrCreateCommand
|
||||||
|
{
|
||||||
|
EmailAddress = receiver.Email,
|
||||||
|
Salution = receiver.FullName,
|
||||||
|
PhoneNumber = string.IsNullOrWhiteSpace(receiver.PhoneNumber)
|
||||||
|
? null
|
||||||
|
: receiver.PhoneNumber,
|
||||||
|
DocReceiverElements = fields,
|
||||||
|
};
|
||||||
|
}).ToList();
|
||||||
|
|
||||||
|
var command = new CreateEnvelopeReceiverCommand
|
||||||
|
{
|
||||||
|
Title = _envelopeTitle.Trim(),
|
||||||
|
Message = string.IsNullOrWhiteSpace(_envelopeMessage)
|
||||||
|
? "Bitte unterzeichnen Sie das beigefügte Dokument."
|
||||||
|
: _envelopeMessage.Trim(),
|
||||||
|
TFAEnabled = false,
|
||||||
|
Document = new DocumentCreateCommand { DataAsBase64 = docBase64 },
|
||||||
|
Receivers = receiversCmd,
|
||||||
|
};
|
||||||
|
|
||||||
|
var result = await EnvelopeReceiverService.CreateAsync(command);
|
||||||
|
|
||||||
|
Logger.LogInformation(
|
||||||
|
"[SenderEditor] Envelope created. Id={Id} SentReceivers={Count}",
|
||||||
|
result?.Id, result?.SentReceiver.Count());
|
||||||
|
|
||||||
|
// Success — show popup; GoToDashboard clears cache and navigates
|
||||||
|
_saveErrorMessage = null;
|
||||||
|
_savePopupVisible = true;
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
Logger.LogError(ex, "[SenderEditor] Failed to create envelope");
|
||||||
|
_saveErrorMessage = ex.Message;
|
||||||
|
_savePopupVisible = true;
|
||||||
|
}
|
||||||
|
finally
|
||||||
|
{
|
||||||
|
_isSaving = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Cache persistence ──
|
||||||
|
void PersistSession()
|
||||||
|
{
|
||||||
|
if (string.IsNullOrWhiteSpace(Esid)) return;
|
||||||
|
|
||||||
|
var data = new EditorSessionData(
|
||||||
|
OriginalPdfBytes: _originalPdfBytes ?? [],
|
||||||
|
Fields: [.. _signatureFields],
|
||||||
|
FileName: _fileName,
|
||||||
|
Receivers: [.. _receivers],
|
||||||
|
Title: _envelopeTitle,
|
||||||
|
Message: _envelopeMessage);
|
||||||
|
|
||||||
|
MemoryCache.Set(SessionKey, data, SessionTtl);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── PdfSharp: burn all placeholder boxes onto the original PDF ──
|
||||||
|
static byte[] DrawPlaceholders(byte[] originalPdf, IReadOnlyList<SignatureFieldDraft> fields)
|
||||||
|
{
|
||||||
|
if (fields.Count == 0) return originalPdf;
|
||||||
|
|
||||||
|
using var inputMs = new System.IO.MemoryStream(originalPdf);
|
||||||
|
using var outputMs = new System.IO.MemoryStream();
|
||||||
|
|
||||||
|
var document = PdfSharp.Pdf.IO.PdfReader.Open(
|
||||||
|
inputMs, PdfSharp.Pdf.IO.PdfDocumentOpenMode.Modify);
|
||||||
|
|
||||||
|
foreach (var field in fields)
|
||||||
|
{
|
||||||
|
int pageIndex = field.Page - 1;
|
||||||
|
if (pageIndex < 0 || pageIndex >= document.PageCount) continue;
|
||||||
|
|
||||||
|
var page = document.Pages[pageIndex];
|
||||||
|
using var gfx = PdfSharp.Drawing.XGraphics.FromPdfPage(page);
|
||||||
|
|
||||||
|
// Derive colours from the receiver's hex colour
|
||||||
|
var fillBrush = new PdfSharp.Drawing.XSolidBrush(HexToXColor(field.Color, alpha: 35));
|
||||||
|
var borderPen = new PdfSharp.Drawing.XPen(HexToXColor(field.Color, alpha: 210), 1.5);
|
||||||
|
var textBrush = new PdfSharp.Drawing.XSolidBrush(HexToXColor(field.Color, alpha: 200));
|
||||||
|
var nameBrush = new PdfSharp.Drawing.XSolidBrush(HexToXColor(field.Color, alpha: 230));
|
||||||
|
|
||||||
|
var fontLabel = new PdfSharp.Drawing.XFont("Arial", 9, PdfSharp.Drawing.XFontStyleEx.Bold);
|
||||||
|
var fontName = new PdfSharp.Drawing.XFont("Arial", 7, PdfSharp.Drawing.XFontStyleEx.Regular);
|
||||||
|
|
||||||
|
var fmtCenter = new PdfSharp.Drawing.XStringFormat
|
||||||
|
{
|
||||||
|
Alignment = PdfSharp.Drawing.XStringAlignment.Center,
|
||||||
|
LineAlignment = PdfSharp.Drawing.XLineAlignment.Center,
|
||||||
|
};
|
||||||
|
|
||||||
|
var rect = new PdfSharp.Drawing.XRect(field.XPt, field.YPt, SigWidthPt, SigHeightPt);
|
||||||
|
|
||||||
|
gfx.DrawRectangle(fillBrush, rect);
|
||||||
|
gfx.DrawRectangle(borderPen, rect);
|
||||||
|
|
||||||
|
// "UNTERSCHRIFT" label centred in upper two-thirds
|
||||||
|
var labelRect = new PdfSharp.Drawing.XRect(
|
||||||
|
field.XPt, field.YPt, SigWidthPt, SigHeightPt * 0.65);
|
||||||
|
gfx.DrawString("UNTERSCHRIFT", fontLabel, textBrush, labelRect, fmtCenter);
|
||||||
|
|
||||||
|
// Receiver name centred in lower third
|
||||||
|
var nameRect = new PdfSharp.Drawing.XRect(
|
||||||
|
field.XPt + 4, field.YPt + SigHeightPt * 0.68,
|
||||||
|
SigWidthPt - 8, SigHeightPt * 0.30);
|
||||||
|
var displayName = field.ReceiverName.Length > 22
|
||||||
|
? field.ReceiverName[..19] + "..."
|
||||||
|
: field.ReceiverName;
|
||||||
|
gfx.DrawString(displayName, fontName, nameBrush, nameRect, fmtCenter);
|
||||||
|
}
|
||||||
|
|
||||||
|
document.Save(outputMs);
|
||||||
|
return outputMs.ToArray();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>Converts a CSS hex colour string (e.g. "#4F46E5") to a PdfSharp XColor.</summary>
|
||||||
|
static PdfSharp.Drawing.XColor HexToXColor(string hex, int alpha)
|
||||||
|
{
|
||||||
|
var c = System.Drawing.ColorTranslator.FromHtml(hex);
|
||||||
|
return PdfSharp.Drawing.XColor.FromArgb(alpha, c.R, c.G, c.B);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Receiver popup ──
|
||||||
void OpenAddReceiverPopup()
|
void OpenAddReceiverPopup()
|
||||||
{
|
{
|
||||||
_receiverDraftName = string.Empty;
|
_receiverDraftName = string.Empty;
|
||||||
@@ -522,37 +894,32 @@
|
|||||||
{
|
{
|
||||||
if (_receiverEmailSuggestions.Count == 0)
|
if (_receiverEmailSuggestions.Count == 0)
|
||||||
{
|
{
|
||||||
if (e.Key == "Escape")
|
if (e.Key == "Escape") _selectedReceiverEmailSuggestion = null;
|
||||||
_selectedReceiverEmailSuggestion = null;
|
|
||||||
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
var currentIndex = _selectedReceiverEmailSuggestion is null
|
var currentIndex = _selectedReceiverEmailSuggestion is null
|
||||||
? -1
|
? -1
|
||||||
: _receiverEmailSuggestions.FindIndex(email => string.Equals(email, _selectedReceiverEmailSuggestion, StringComparison.OrdinalIgnoreCase));
|
: _receiverEmailSuggestions.FindIndex(em =>
|
||||||
|
string.Equals(em, _selectedReceiverEmailSuggestion, StringComparison.OrdinalIgnoreCase));
|
||||||
|
|
||||||
if (e.Key == "ArrowDown")
|
if (e.Key == "ArrowDown")
|
||||||
{
|
{
|
||||||
var nextIndex = currentIndex < _receiverEmailSuggestions.Count - 1 ? currentIndex + 1 : 0;
|
var next = currentIndex < _receiverEmailSuggestions.Count - 1 ? currentIndex + 1 : 0;
|
||||||
await OnReceiverEmailSuggestionSelectedAsync(_receiverEmailSuggestions[nextIndex]);
|
SelectReceiverEmailSuggestion(_receiverEmailSuggestions[next]);
|
||||||
}
|
}
|
||||||
else if (e.Key == "ArrowUp")
|
else if (e.Key == "ArrowUp")
|
||||||
{
|
{
|
||||||
var nextIndex = currentIndex > 0 ? currentIndex - 1 : _receiverEmailSuggestions.Count - 1;
|
var next = currentIndex > 0 ? currentIndex - 1 : _receiverEmailSuggestions.Count - 1;
|
||||||
await OnReceiverEmailSuggestionSelectedAsync(_receiverEmailSuggestions[nextIndex]);
|
SelectReceiverEmailSuggestion(_receiverEmailSuggestions[next]);
|
||||||
}
|
}
|
||||||
else if (e.Key == "Enter")
|
else if (e.Key == "Enter")
|
||||||
{
|
{
|
||||||
var selectedValue = currentIndex >= 0 && currentIndex < _receiverEmailSuggestions.Count
|
var sel = currentIndex >= 0 && currentIndex < _receiverEmailSuggestions.Count
|
||||||
? _receiverEmailSuggestions[currentIndex]
|
? _receiverEmailSuggestions[currentIndex]
|
||||||
: _receiverEmailSuggestions.FirstOrDefault();
|
: _receiverEmailSuggestions.FirstOrDefault();
|
||||||
|
if (!string.IsNullOrWhiteSpace(sel))
|
||||||
if (!string.IsNullOrWhiteSpace(selectedValue))
|
await OnReceiverEmailSuggestionCommittedAsync(sel);
|
||||||
{
|
|
||||||
await OnReceiverEmailSuggestionSelectedAsync(selectedValue);
|
|
||||||
_receiverEmailSuggestions.Clear();
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
else if (e.Key == "Escape")
|
else if (e.Key == "Escape")
|
||||||
{
|
{
|
||||||
@@ -561,14 +928,18 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Task OnReceiverEmailSuggestionSelectedAsync(string? value)
|
void SelectReceiverEmailSuggestion(string? value)
|
||||||
{
|
{
|
||||||
if (string.IsNullOrWhiteSpace(value))
|
if (string.IsNullOrWhiteSpace(value)) return;
|
||||||
return Task.CompletedTask;
|
|
||||||
|
|
||||||
_selectedReceiverEmailSuggestion = value.Trim();
|
_selectedReceiverEmailSuggestion = value.Trim();
|
||||||
_receiverDraftEmail = _selectedReceiverEmailSuggestion;
|
_receiverDraftEmail = _selectedReceiverEmailSuggestion;
|
||||||
_receiverPopupValidationMessage = null;
|
_receiverPopupValidationMessage = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
Task OnReceiverEmailSuggestionCommittedAsync(string? value)
|
||||||
|
{
|
||||||
|
SelectReceiverEmailSuggestion(value);
|
||||||
|
_receiverEmailSuggestions.Clear();
|
||||||
return Task.CompletedTask;
|
return Task.CompletedTask;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -593,19 +964,17 @@
|
|||||||
try
|
try
|
||||||
{
|
{
|
||||||
var results = await ReceiverPageDataService.SearchReceiverEMailsAsync(_receiverDraftEmail);
|
var results = await ReceiverPageDataService.SearchReceiverEMailsAsync(_receiverDraftEmail);
|
||||||
|
if (searchVersion != _receiverEmailSearchVersion) return;
|
||||||
if (searchVersion != _receiverEmailSearchVersion)
|
|
||||||
return;
|
|
||||||
|
|
||||||
_receiverEmailSuggestions = results
|
_receiverEmailSuggestions = results
|
||||||
.Where(email => !string.IsNullOrWhiteSpace(email))
|
.Where(em => !string.IsNullOrWhiteSpace(em))
|
||||||
.Distinct(StringComparer.OrdinalIgnoreCase)
|
.Distinct(StringComparer.OrdinalIgnoreCase)
|
||||||
.OrderBy(email => email)
|
.OrderBy(em => em)
|
||||||
.Take(12)
|
.Take(12)
|
||||||
.ToList();
|
.ToList();
|
||||||
|
|
||||||
_selectedReceiverEmailSuggestion = _receiverEmailSuggestions.FirstOrDefault(email =>
|
_selectedReceiverEmailSuggestion = _receiverEmailSuggestions.FirstOrDefault(em =>
|
||||||
string.Equals(email, _receiverDraftEmail, StringComparison.OrdinalIgnoreCase));
|
string.Equals(em, _receiverDraftEmail, StringComparison.OrdinalIgnoreCase));
|
||||||
}
|
}
|
||||||
catch (Exception ex)
|
catch (Exception ex)
|
||||||
{
|
{
|
||||||
@@ -634,50 +1003,54 @@
|
|||||||
_receiverPopupValidationMessage = "Bitte geben Sie einen Vor- und Nachnamen ein.";
|
_receiverPopupValidationMessage = "Bitte geben Sie einen Vor- und Nachnamen ein.";
|
||||||
return Task.CompletedTask;
|
return Task.CompletedTask;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (string.IsNullOrWhiteSpace(email))
|
if (string.IsNullOrWhiteSpace(email))
|
||||||
{
|
{
|
||||||
_receiverPopupValidationMessage = "Bitte geben Sie eine E-Mail-Adresse ein.";
|
_receiverPopupValidationMessage = "Bitte geben Sie eine E-Mail-Adresse ein.";
|
||||||
return Task.CompletedTask;
|
return Task.CompletedTask;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!ReceiverEmailValidator.IsValid(email))
|
if (!ReceiverEmailValidator.IsValid(email))
|
||||||
{
|
{
|
||||||
_receiverPopupValidationMessage = "Bitte geben Sie eine gültige E-Mail-Adresse ein.";
|
_receiverPopupValidationMessage = "Bitte geben Sie eine gültige E-Mail-Adresse ein.";
|
||||||
return Task.CompletedTask;
|
return Task.CompletedTask;
|
||||||
}
|
}
|
||||||
|
if (_receivers.Any(r => string.Equals(r.Email, email, StringComparison.OrdinalIgnoreCase)))
|
||||||
if (_receivers.Any(receiver => string.Equals(receiver.Email, email, StringComparison.OrdinalIgnoreCase)))
|
|
||||||
{
|
{
|
||||||
_receiverPopupValidationMessage = "Diese E-Mail-Adresse wurde bereits hinzugefügt.";
|
_receiverPopupValidationMessage = "Diese E-Mail-Adresse wurde bereits hinzugefügt.";
|
||||||
return Task.CompletedTask;
|
return Task.CompletedTask;
|
||||||
}
|
}
|
||||||
|
|
||||||
_receivers.Add(new ReceiverDraft(Guid.NewGuid(), fullName, email, phoneNumber));
|
_receivers.Add(new ReceiverDraft(Guid.NewGuid(), fullName, email, phoneNumber,
|
||||||
|
ReceiverPalette[_receivers.Count % ReceiverPalette.Length]));
|
||||||
|
PersistSession();
|
||||||
CloseAddReceiverPopup();
|
CloseAddReceiverPopup();
|
||||||
return Task.CompletedTask;
|
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)
|
|
||||||
return;
|
|
||||||
|
|
||||||
await JSRuntime.InvokeVoidAsync(
|
|
||||||
"envelopeEditor.syncOverlayToPage",
|
|
||||||
"pdf-editor-wrapper",
|
|
||||||
"pdf-editor-overlay");
|
|
||||||
}
|
|
||||||
|
|
||||||
// ── Models ──
|
// ── Models ──
|
||||||
record SignatureFieldDraft(double XPt, double YPt, int Page, double DisplayX, double DisplayY);
|
record SignatureFieldDraft(double XPt, double YPt, int Page, string ReceiverName, string Color);
|
||||||
|
|
||||||
record OverlayCoords(double RelX, double RelY, double ContainerW, double ContainerH);
|
record NormalisedCoords(double NormX, double NormY, int PageIndex);
|
||||||
|
|
||||||
record ReceiverDraft(Guid Id, string FullName, string Email, string PhoneNumber);
|
record ReceiverDraft(Guid Id, string FullName, string Email, string PhoneNumber, string Color);
|
||||||
|
|
||||||
|
record EditorSessionData(
|
||||||
|
byte[] OriginalPdfBytes,
|
||||||
|
List<SignatureFieldDraft> Fields,
|
||||||
|
string FileName,
|
||||||
|
List<ReceiverDraft> Receivers,
|
||||||
|
string Title,
|
||||||
|
string Message);
|
||||||
|
|
||||||
|
// ── 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
|
||||||
|
];
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
@page "/envelope/{EnvelopeKey}/report"
|
@page "/envelope/{EnvelopeKey}"
|
||||||
@rendermode InteractiveServer
|
@rendermode InteractiveServer
|
||||||
@using DevExpress.Blazor.Reporting
|
@using DevExpress.Blazor.Reporting
|
||||||
@using DevExpress.XtraReports.UI
|
@using DevExpress.XtraReports.UI
|
||||||
@@ -18,7 +18,7 @@
|
|||||||
@inject EnvelopeGenerator.Server.Services.EnvelopeReceiverPageDataService PageDataService
|
@inject EnvelopeGenerator.Server.Services.EnvelopeReceiverPageDataService PageDataService
|
||||||
@inject AppVersionService AppVersion
|
@inject AppVersionService AppVersion
|
||||||
@inject IMemoryCache MemoryCache
|
@inject IMemoryCache MemoryCache
|
||||||
@inject ILogger<EnvelopeReceiverReportPage> Logger
|
@inject ILogger<ReceiverPage> Logger
|
||||||
@implements IDisposable
|
@implements IDisposable
|
||||||
|
|
||||||
<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" />
|
||||||
@@ -14,7 +14,7 @@
|
|||||||
@inject EnvelopeGenerator.Server.Services.EnvelopeReceiverPageDataService PageDataService
|
@inject EnvelopeGenerator.Server.Services.EnvelopeReceiverPageDataService PageDataService
|
||||||
@inject AppVersionService AppVersion
|
@inject AppVersionService AppVersion
|
||||||
@inject IMemoryCache MemoryCache
|
@inject IMemoryCache MemoryCache
|
||||||
@inject ILogger<EnvelopeReceiverReportSignedPage> Logger
|
@inject ILogger<ReceiverSignedPage> Logger
|
||||||
@implements IDisposable
|
@implements IDisposable
|
||||||
|
|
||||||
<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" />
|
||||||
@@ -205,7 +205,7 @@
|
|||||||
"[SignedPage] Cache miss or no sid={Sid} for {EnvelopeKey}, redirecting to report page.",
|
"[SignedPage] Cache miss or no sid={Sid} for {EnvelopeKey}, redirecting to report page.",
|
||||||
Sid, EnvelopeKey);
|
Sid, EnvelopeKey);
|
||||||
Navigation.NavigateTo(
|
Navigation.NavigateTo(
|
||||||
$"/envelope/{Uri.EscapeDataString(EnvelopeKey)}/report",
|
$"/envelope/{Uri.EscapeDataString(EnvelopeKey)}",
|
||||||
forceLoad: true);
|
forceLoad: true);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
@page "/envelope/{EnvelopeKey}"
|
@page "/envelope/{EnvelopeKey}/deprc"
|
||||||
@rendermode InteractiveServer
|
@rendermode InteractiveServer
|
||||||
@using EnvelopeGenerator.Server.Client.Models
|
@using EnvelopeGenerator.Server.Client.Models
|
||||||
@using EnvelopeGenerator.Server.Client.Models.Constants
|
@using EnvelopeGenerator.Server.Client.Models.Constants
|
||||||
@@ -40,7 +40,7 @@ public partial class AuthController(IOptions<AuthTokenKeys> authTokenKeyOptions,
|
|||||||
/// <response code="401">Wenn es kein zugelassenes Cookie gibt, wird „nicht zugelassen“ zurückgegeben.</response>
|
/// <response code="401">Wenn es kein zugelassenes Cookie gibt, wird „nicht zugelassen“ zurückgegeben.</response>
|
||||||
[ProducesResponseType(typeof(void), StatusCodes.Status200OK)]
|
[ProducesResponseType(typeof(void), StatusCodes.Status200OK)]
|
||||||
[ProducesResponseType(typeof(void), StatusCodes.Status401Unauthorized)]
|
[ProducesResponseType(typeof(void), StatusCodes.Status401Unauthorized)]
|
||||||
[Authorize(Policy = AuthPolicy.Sender)]
|
[Authorize(AuthenticationSchemes = AuthScheme.Sender)]
|
||||||
[HttpPost("logout")]
|
[HttpPost("logout")]
|
||||||
public IActionResult Logout()
|
public IActionResult Logout()
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -30,7 +30,7 @@ public class DocumentController(IMediator mediator, IAuthorizationService authSe
|
|||||||
/// <param name="query">Encoded envelope key.</param>
|
/// <param name="query">Encoded envelope key.</param>
|
||||||
/// <param name="cancel">Cancellation token.</param>
|
/// <param name="cancel">Cancellation token.</param>
|
||||||
[HttpGet]
|
[HttpGet]
|
||||||
[Authorize(Policy = AuthPolicy.Sender)]
|
[Authorize(AuthenticationSchemes = AuthScheme.Sender)]
|
||||||
public async Task<IActionResult> GetDocument(CancellationToken cancel, [FromQuery] ReadDocumentQuery? query = null)
|
public async Task<IActionResult> GetDocument(CancellationToken cancel, [FromQuery] ReadDocumentQuery? query = null)
|
||||||
{
|
{
|
||||||
if (query is null)
|
if (query is null)
|
||||||
|
|||||||
@@ -24,7 +24,7 @@ namespace EnvelopeGenerator.Server.Controllers;
|
|||||||
/// </param>
|
/// </param>
|
||||||
[Route("api/[controller]")]
|
[Route("api/[controller]")]
|
||||||
[ApiController]
|
[ApiController]
|
||||||
[Authorize(Policy = AuthPolicy.Sender)]
|
[Authorize(AuthenticationSchemes = AuthScheme.Sender)]
|
||||||
public class EmailTemplateController(IMediator mediator) : ControllerBase
|
public class EmailTemplateController(IMediator mediator) : ControllerBase
|
||||||
{
|
{
|
||||||
/// <summary>
|
/// <summary>
|
||||||
|
|||||||
@@ -150,7 +150,7 @@ public class EnvelopeReceiverController : ControllerBase
|
|||||||
/// <response code="400">Wenn ein Fehler im HTTP-Body auftritt</response>
|
/// <response code="400">Wenn ein Fehler im HTTP-Body auftritt</response>
|
||||||
/// <response code="401">Wenn kein autorisierter Token vorhanden ist</response>
|
/// <response code="401">Wenn kein autorisierter Token vorhanden ist</response>
|
||||||
/// <response code="500">Es handelt sich um einen unerwarteten Fehler. Die Protokolle sollten überprüft werden.</response>
|
/// <response code="500">Es handelt sich um einen unerwarteten Fehler. Die Protokolle sollten überprüft werden.</response>
|
||||||
[Authorize]
|
[Authorize(AuthenticationSchemes = AuthScheme.Sender)]
|
||||||
[HttpPost]
|
[HttpPost]
|
||||||
public async Task<IActionResult> CreateAsync([FromBody] CreateEnvelopeReceiverCommand request, CancellationToken cancel)
|
public async Task<IActionResult> CreateAsync([FromBody] CreateEnvelopeReceiverCommand request, CancellationToken cancel)
|
||||||
{
|
{
|
||||||
@@ -214,6 +214,10 @@ public class EnvelopeReceiverController : ControllerBase
|
|||||||
if (reader.Read())
|
if (reader.Read())
|
||||||
{
|
{
|
||||||
bool outSuccess = reader.GetBoolean(0);
|
bool outSuccess = reader.GetBoolean(0);
|
||||||
|
if (!outSuccess)
|
||||||
|
_logger.LogWarning(
|
||||||
|
"PRSIG_API_ADD_DOC_RECEIVER_ELEM returned OUT_SUCCESS=false. DOC_ID={DocId}, RECEIVER_ID={ReceiverId}, Page={Page}",
|
||||||
|
document.Id, rcv.Id, sign.Page);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
#endregion
|
#endregion
|
||||||
@@ -221,8 +225,6 @@ public class EnvelopeReceiverController : ControllerBase
|
|||||||
#region Create history
|
#region Create history
|
||||||
// ENV_UID, STATUS_ID, USER_ID,
|
// ENV_UID, STATUS_ID, USER_ID,
|
||||||
string sql_hist = @"
|
string sql_hist = @"
|
||||||
USE [DD_ECM]
|
|
||||||
|
|
||||||
DECLARE @OUT_SUCCESS bit;
|
DECLARE @OUT_SUCCESS bit;
|
||||||
|
|
||||||
EXEC [dbo].[PRSIG_API_ADD_HISTORY_STATE]
|
EXEC [dbo].[PRSIG_API_ADD_HISTORY_STATE]
|
||||||
@@ -244,6 +246,10 @@ public class EnvelopeReceiverController : ControllerBase
|
|||||||
if (reader.Read())
|
if (reader.Read())
|
||||||
{
|
{
|
||||||
bool outSuccess = reader.GetBoolean(0);
|
bool outSuccess = reader.GetBoolean(0);
|
||||||
|
if (!outSuccess)
|
||||||
|
_logger.LogWarning(
|
||||||
|
"PRSIG_API_ADD_HISTORY_STATE returned OUT_SUCCESS=false. EnvelopeUuid={EnvelopeUuid}",
|
||||||
|
envelope.Uuid);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
#endregion
|
#endregion
|
||||||
|
|||||||
@@ -1,9 +1,20 @@
|
|||||||
<Project Sdk="Microsoft.NET.Sdk.Web">
|
<Project Sdk="Microsoft.NET.Sdk.Web">
|
||||||
|
|
||||||
<PropertyGroup>
|
<PropertyGroup>
|
||||||
<TargetFramework>net8.0</TargetFramework>
|
<TargetFrameworks>net8.0</TargetFrameworks>
|
||||||
<Nullable>enable</Nullable>
|
<Nullable>enable</Nullable>
|
||||||
<ImplicitUsings>enable</ImplicitUsings>
|
<ImplicitUsings>enable</ImplicitUsings>
|
||||||
|
<GenerateDocumentationFile>true</GenerateDocumentationFile>
|
||||||
|
<PackageId>EnvelopeGenerator.Server</PackageId>
|
||||||
|
<Title></Title>
|
||||||
|
<Authors>Digital Data GmbH</Authors>
|
||||||
|
<Company>Digital Data GmbH</Company>
|
||||||
|
<Product>EnvelopeGenerator.Server</Product>
|
||||||
|
<Version>1.0.1-beta</Version>
|
||||||
|
<FileVersion>1.0.1.0</FileVersion>
|
||||||
|
<AssemblyVersion>1.0.1.0</AssemblyVersion>
|
||||||
|
<Copyright>Copyright © 2026 Digital Data GmbH. All rights reserved.</Copyright>
|
||||||
|
<DocumentationFile>bin\$(Configuration)\$(TargetFramework)\$(AssemblyName).xml</DocumentationFile>
|
||||||
</PropertyGroup>
|
</PropertyGroup>
|
||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
|
|||||||
@@ -0,0 +1,33 @@
|
|||||||
|
namespace EnvelopeGenerator.Server.Handlers;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// A <see cref="DelegatingHandler"/> that forwards the incoming HTTP request's
|
||||||
|
/// <c>Cookie</c> header to all outgoing <see cref="System.Net.Http.HttpClient"/> calls
|
||||||
|
/// made by Blazor Server components.
|
||||||
|
///
|
||||||
|
/// Problem it solves:
|
||||||
|
/// Blazor Server runs on the server process. When a component calls an API endpoint
|
||||||
|
/// that requires cookie-based JWT authentication (AuthScheme.Sender), the HttpClient
|
||||||
|
/// does not automatically include the browser's cookies — those only travel with
|
||||||
|
/// browser-initiated requests. This handler copies the <c>Cookie</c> header from the
|
||||||
|
/// current <see cref="IHttpContextAccessor.HttpContext"/> into every outgoing request
|
||||||
|
/// so that the API's JwtBearer <c>OnMessageReceived</c> callback can extract the token.
|
||||||
|
///
|
||||||
|
/// Thread safety:
|
||||||
|
/// The handler is registered as Transient and is resolved per-request by the
|
||||||
|
/// IHttpClientFactory pipeline, so there is no shared state between requests.
|
||||||
|
/// </summary>
|
||||||
|
public class SenderAuthCookieHandler(IHttpContextAccessor httpContextAccessor) : DelegatingHandler
|
||||||
|
{
|
||||||
|
protected override Task<HttpResponseMessage> SendAsync(
|
||||||
|
HttpRequestMessage request,
|
||||||
|
CancellationToken cancellationToken)
|
||||||
|
{
|
||||||
|
var cookieHeader = httpContextAccessor.HttpContext?.Request.Headers["Cookie"].ToString();
|
||||||
|
|
||||||
|
if (!string.IsNullOrWhiteSpace(cookieHeader))
|
||||||
|
request.Headers.TryAddWithoutValidation("Cookie", cookieHeader);
|
||||||
|
|
||||||
|
return base.SendAsync(request, cancellationToken);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -69,6 +69,9 @@ try
|
|||||||
builder.Services.AddHttpContextAccessor();
|
builder.Services.AddHttpContextAccessor();
|
||||||
|
|
||||||
// Named HttpClient for internal API calls
|
// Named HttpClient for internal API calls
|
||||||
|
// SenderAuthCookieHandler forwards the browser's Cookie header so that
|
||||||
|
// Blazor Server components can call cookie-authenticated endpoints (AuthScheme.Sender).
|
||||||
|
builder.Services.AddTransient<EnvelopeGenerator.Server.Handlers.SenderAuthCookieHandler>();
|
||||||
builder.Services.AddHttpClient("EnvelopeGenerator.Server", (sp, client) =>
|
builder.Services.AddHttpClient("EnvelopeGenerator.Server", (sp, client) =>
|
||||||
{
|
{
|
||||||
var httpContextAccessor = sp.GetRequiredService<IHttpContextAccessor>();
|
var httpContextAccessor = sp.GetRequiredService<IHttpContextAccessor>();
|
||||||
@@ -79,7 +82,8 @@ try
|
|||||||
// Set base address to current host for SSR scenarios
|
// Set base address to current host for SSR scenarios
|
||||||
client.BaseAddress = new Uri($"{request.Scheme}://{request.Host}");
|
client.BaseAddress = new Uri($"{request.Scheme}://{request.Host}");
|
||||||
}
|
}
|
||||||
});
|
})
|
||||||
|
.AddHttpMessageHandler<EnvelopeGenerator.Server.Handlers.SenderAuthCookieHandler>();
|
||||||
|
|
||||||
// CORS Policy
|
// CORS Policy
|
||||||
var allowedOrigins = config.GetSection("AllowedOrigins").Get<string[]>() ??
|
var allowedOrigins = config.GetSection("AllowedOrigins").Get<string[]>() ??
|
||||||
|
|||||||
@@ -0,0 +1,23 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<!--
|
||||||
|
https://go.microsoft.com/fwlink/?LinkID=208121.
|
||||||
|
-->
|
||||||
|
<Project>
|
||||||
|
<PropertyGroup>
|
||||||
|
<WebPublishMethod>Package</WebPublishMethod>
|
||||||
|
<LastUsedBuildConfiguration>Release</LastUsedBuildConfiguration>
|
||||||
|
<LastUsedPlatform>Any CPU</LastUsedPlatform>
|
||||||
|
<SiteUrlToLaunchAfterPublish />
|
||||||
|
<LaunchSiteAfterPublish>true</LaunchSiteAfterPublish>
|
||||||
|
<ExcludeApp_Data>false</ExcludeApp_Data>
|
||||||
|
<ProjectGuid>5e0e17c0-ff5a-4246-bf87-1add85376a27</ProjectGuid>
|
||||||
|
<DesktopBuildPackageLocation>M:\App&Service\0 DD - Smart UP\signFLOW\API\net8\$(Version)\EnvelopeGenerator.Server.zip</DesktopBuildPackageLocation>
|
||||||
|
<PackageAsSingleFile>true</PackageAsSingleFile>
|
||||||
|
<DeployIisAppPath>EnvelopeGenerator</DeployIisAppPath>
|
||||||
|
<_TargetId>IISWebDeployPackage</_TargetId>
|
||||||
|
<TargetFramework>net8.0</TargetFramework>
|
||||||
|
<SelfContained>true</SelfContained>
|
||||||
|
<RuntimeIdentifier>win-x64</RuntimeIdentifier>
|
||||||
|
<EnvironmentName>Production</EnvironmentName>
|
||||||
|
</PropertyGroup>
|
||||||
|
</Project>
|
||||||
225
EnvelopeGenerator.Server/EnvelopeGenerator.Server/README.md
Normal file
225
EnvelopeGenerator.Server/EnvelopeGenerator.Server/README.md
Normal file
@@ -0,0 +1,225 @@
|
|||||||
|
# EnvelopeGenerator.Server — Publish & Deployment Guide
|
||||||
|
|
||||||
|
## Inhaltsverzeichnis
|
||||||
|
|
||||||
|
1. [Unterschied zu einer normalen ASP.NET Core API](#unterschied-zu-einer-normalen-aspnet-core-api)
|
||||||
|
2. [Warum Self-Contained Publish?](#warum-self-contained-publish)
|
||||||
|
3. [Publish-Befehl (Terminal)](#publish-befehl-terminal)
|
||||||
|
4. [IIS-Konfiguration](#iis-konfiguration)
|
||||||
|
5. [Verzeichnisstruktur nach dem Publish](#verzeichnisstruktur-nach-dem-publish)
|
||||||
|
6. [Häufige Fehler](#häufige-fehler)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Unterschied zu einer normalen ASP.NET Core API
|
||||||
|
|
||||||
|
`EnvelopeGenerator.Server` ist **keine** gewöhnliche ASP.NET Core Web API. Es handelt sich um eine **Blazor Auto (Server + WebAssembly Hybrid)**-Anwendung.
|
||||||
|
|
||||||
|
| Merkmal | Normale ASP.NET Core API | EnvelopeGenerator.Server (Blazor Auto) |
|
||||||
|
|---|---|---|
|
||||||
|
| Projekttyp | `Microsoft.NET.Sdk.Web` | `Microsoft.NET.Sdk.Web` + WASM Client |
|
||||||
|
| Frontend | Keins / Razor Pages | Blazor Server + Blazor WASM |
|
||||||
|
| WASM-Komponente | Nein | Ja (`EnvelopeGenerator.Server.Client`) |
|
||||||
|
| Framework-DLL-Bindung | Tolerant gegenüber Runtime-Versionen | **Strikt**: WASM erwartet exakte Assembly-Versionen |
|
||||||
|
| Publish ohne .NET auf Zielserver | Nicht nötig (FDD reicht meist) | **Self-Contained Pflicht** empfohlen |
|
||||||
|
| IIS Application Pool | `.NET CLR v4.0` oder `No Managed Code` | **Zwingend: `No Managed Code`** |
|
||||||
|
| Publish-Paketgröße | ~5–20 MB | **~500 MB** (enthält .NET Runtime) |
|
||||||
|
| `web.config processPath` | `dotnet` + `.dll` | **`.\EnvelopeGenerator.Server.exe`** |
|
||||||
|
|
||||||
|
### Warum die WASM-Komponente den Unterschied macht
|
||||||
|
|
||||||
|
Die WASM-Seite der Anwendung (`EnvelopeGenerator.Server.Client`) bindet Assemblies wie
|
||||||
|
`Microsoft.Extensions.DependencyInjection.Abstractions` in einer **fest definierten Version**.
|
||||||
|
Bei einem **Framework-Dependent Deployment** werden diese Assemblies nicht mitgeliefert und
|
||||||
|
müssen auf dem Zielserver vorhanden sein — in der exakt passenden Version.
|
||||||
|
|
||||||
|
Fehlt die passende .NET-Runtime auf dem Zielserver, erscheint folgender Fehler beim Start:
|
||||||
|
|
||||||
|
```
|
||||||
|
Unhandled exception. System.IO.FileNotFoundException:
|
||||||
|
Could not load file or assembly
|
||||||
|
'Microsoft.Extensions.DependencyInjection.Abstractions, Version=8.0.0.0'
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Warum Self-Contained Publish?
|
||||||
|
|
||||||
|
Beim **Self-Contained Deployment** werden **alle benötigten .NET Runtime-DLLs** in das
|
||||||
|
Ausgabeverzeichnis kopiert. Die Anwendung ist damit vollständig unabhängig von der auf dem
|
||||||
|
Zielserver installierten .NET-Version.
|
||||||
|
|
||||||
|
| | Framework-Dependent | Self-Contained |
|
||||||
|
|---|---|---|
|
||||||
|
| .NET auf Zielserver nötig | Ja | **Nein** |
|
||||||
|
| Paketgröße | ~20 MB | ~500 MB |
|
||||||
|
| `runtimeconfig.json` | `frameworkVersion` vorhanden | `includedFrameworks` (Runtime eingebettet) |
|
||||||
|
| Fehleranfälligkeit auf Fremd-PC | Hoch | Minimal |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Publish-Befehl (Terminal)
|
||||||
|
|
||||||
|
### Empfohlener Befehl (Self-Contained, win-x64)
|
||||||
|
|
||||||
|
```bat
|
||||||
|
dotnet publish EnvelopeGenerator.Server\EnvelopeGenerator.Server\EnvelopeGenerator.Server.csproj ^
|
||||||
|
-c Release ^
|
||||||
|
-f net8.0 ^
|
||||||
|
--self-contained true ^
|
||||||
|
--runtime win-x64 ^
|
||||||
|
-o .\publish-output
|
||||||
|
```
|
||||||
|
|
||||||
|
> Dieser Befehl muss vom **Solution-Root-Verzeichnis** aus ausgefuehrt werden.
|
||||||
|
> Alternativ: `publish.bat` im selben Verzeichnis wie diese README ausfuehren.
|
||||||
|
|
||||||
|
### Parameter-Erklaerung
|
||||||
|
|
||||||
|
| Parameter | Bedeutung |
|
||||||
|
|---|---|
|
||||||
|
| `-c Release` | Release-Konfiguration (optimiert, kein Debug-Code) |
|
||||||
|
| `-f net8.0` | Ziel-Framework explizit angeben (Pflicht, da `<TargetFrameworks>` mehrere Werte haben kann) |
|
||||||
|
| `--self-contained true` | Alle .NET Runtime-DLLs ins Ausgabeverzeichnis kopieren |
|
||||||
|
| `--runtime win-x64` | Zielplattform: Windows 64-Bit |
|
||||||
|
| `-o .\publish-output` | Ausgabeverzeichnis |
|
||||||
|
|
||||||
|
### Doğrulama nach dem Publish
|
||||||
|
|
||||||
|
Nach erfolgreichem Publish folgende Dateien im Ausgabeverzeichnis prüfen:
|
||||||
|
|
||||||
|
```powershell
|
||||||
|
# Diese Dateien MÜSSEN vorhanden sein (Self-Contained-Nachweis):
|
||||||
|
Test-Path ".\publish-output\hostfxr.dll" # .NET Host
|
||||||
|
Test-Path ".\publish-output\coreclr.dll" # .NET Core Runtime
|
||||||
|
Test-Path ".\publish-output\Microsoft.Extensions.DependencyInjection.Abstractions.dll"
|
||||||
|
Test-Path ".\publish-output\EnvelopeGenerator.Server.exe"
|
||||||
|
Test-Path ".\publish-output\web.config"
|
||||||
|
```
|
||||||
|
|
||||||
|
Alle Ergebnisse müssen `True` sein.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## IIS-Konfiguration
|
||||||
|
|
||||||
|
> **WICHTIG:** Diese Einstellungen unterscheiden sich von einer normalen ASP.NET Core API
|
||||||
|
> und sind zwingend erforderlich.
|
||||||
|
|
||||||
|
### 1. Application Pool — `No Managed Code`
|
||||||
|
|
||||||
|
ASP.NET Core (und damit auch Blazor) verwaltet seinen eigenen Runtime-Lifecycle.
|
||||||
|
IIS darf **keinen** .NET CLR-Managed-Code-Kontext aktivieren.
|
||||||
|
|
||||||
|
**Einstellung:**
|
||||||
|
|
||||||
|
```
|
||||||
|
IIS Manager
|
||||||
|
→ Application Pools
|
||||||
|
→ [Pool-Name der Anwendung] → Basic Settings
|
||||||
|
→ .NET CLR Version: "No Managed Code" ← ZWINGEND
|
||||||
|
```
|
||||||
|
|
||||||
|
> **Fehler bei falscher Einstellung:** HTTP 500.30 — ASP.NET Core app failed to start
|
||||||
|
> (sc-win32-status: 574 in IIS-Logs)
|
||||||
|
|
||||||
|
### 2. ASP.NET Core Module V2
|
||||||
|
|
||||||
|
Das IIS-Modul `AspNetCoreModuleV2` muss installiert sein.
|
||||||
|
Es wird über das **.NET Hosting Bundle** mitgeliefert.
|
||||||
|
|
||||||
|
Prüfen:
|
||||||
|
```
|
||||||
|
IIS Manager → Modules → "AspNetCoreModuleV2" vorhanden?
|
||||||
|
```
|
||||||
|
|
||||||
|
Falls nicht installiert: [.NET 8 Hosting Bundle herunterladen](https://dotnet.microsoft.com/en-us/download/dotnet/8.0)
|
||||||
|
und nach der Installation IIS neu starten:
|
||||||
|
|
||||||
|
```cmd
|
||||||
|
net stop was /y && net start w3svc
|
||||||
|
```
|
||||||
|
|
||||||
|
### 3. web.config — Korrekte Konfiguration für Self-Contained
|
||||||
|
|
||||||
|
Nach dem Publish wird `web.config` automatisch generiert. Für Self-Contained muss sie so aussehen:
|
||||||
|
|
||||||
|
```xml
|
||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<configuration>
|
||||||
|
<location path="." inheritInChildApplications="false">
|
||||||
|
<system.webServer>
|
||||||
|
<handlers>
|
||||||
|
<add name="aspNetCore" path="*" verb="*"
|
||||||
|
modules="AspNetCoreModuleV2" resourceType="Unspecified" />
|
||||||
|
</handlers>
|
||||||
|
<aspNetCore processPath=".\EnvelopeGenerator.Server.exe"
|
||||||
|
stdoutLogEnabled="false"
|
||||||
|
stdoutLogFile=".\logs\stdout"
|
||||||
|
hostingModel="inprocess" />
|
||||||
|
</system.webServer>
|
||||||
|
</location>
|
||||||
|
</configuration>
|
||||||
|
```
|
||||||
|
|
||||||
|
**Kritische Unterschiede zu Framework-Dependent:**
|
||||||
|
|
||||||
|
| Eigenschaft | Framework-Dependent (FALSCH) | Self-Contained (RICHTIG) |
|
||||||
|
|---|---|---|
|
||||||
|
| `processPath` | `dotnet` | `.\EnvelopeGenerator.Server.exe` |
|
||||||
|
| `arguments` | `.\EnvelopeGenerator.Server.dll` | *(leer oder weggelassen)* |
|
||||||
|
|
||||||
|
### 4. Berechtigungen für das `logs`-Verzeichnis
|
||||||
|
|
||||||
|
Wenn `stdoutLogEnabled="true"` gesetzt wird (zur Fehlerdiagnose), muss das `logs`-Verzeichnis
|
||||||
|
existieren und der IIS-Prozess muss Schreibrechte haben:
|
||||||
|
|
||||||
|
```powershell
|
||||||
|
New-Item -ItemType Directory -Path "C:\inetpub\wwwroot\<App-Pfad>\logs" -Force
|
||||||
|
icacls "C:\inetpub\wwwroot\<App-Pfad>\logs" /grant "IIS_IUSRS:(OI)(CI)F"
|
||||||
|
```
|
||||||
|
|
||||||
|
> Ohne dieses Verzeichnis kann die Anwendung bei aktiviertem Logging **nicht starten**.
|
||||||
|
|
||||||
|
### 5. Application Pool Recycle nach Deployment
|
||||||
|
|
||||||
|
Nach jedem Deployment den Application Pool neu starten:
|
||||||
|
|
||||||
|
```cmd
|
||||||
|
# IIS Manager → Application Pools → [Pool] → Recycle
|
||||||
|
# oder per Kommandozeile (als Administrator):
|
||||||
|
%windir%\system32\inetsrv\appcmd recycle apppool /apppool.name:"<Pool-Name>"
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Verzeichnisstruktur nach dem Publish
|
||||||
|
|
||||||
|
```
|
||||||
|
publish-output\
|
||||||
|
├── EnvelopeGenerator.Server.exe ← Startpunkt (Self-Contained)
|
||||||
|
├── EnvelopeGenerator.Server.dll ← Managed Assembly
|
||||||
|
├── EnvelopeGenerator.Server.runtimeconfig.json
|
||||||
|
├── EnvelopeGenerator.Server.deps.json
|
||||||
|
├── web.config ← IIS-Konfiguration (auto-generiert)
|
||||||
|
├── hostfxr.dll ← .NET Host (Self-Contained-Nachweis)
|
||||||
|
├── coreclr.dll ← .NET Core Runtime
|
||||||
|
├── Microsoft.Extensions.*.dll ← Framework-DLLs (jetzt enthalten!)
|
||||||
|
├── DevExpress.*.dll ← UI-Komponenten
|
||||||
|
├── wwwroot\ ← Statische Web-Assets
|
||||||
|
│ ├── _framework\ ← WASM-Binaries
|
||||||
|
│ └── ...
|
||||||
|
└── logs\ ← Stdout-Logs (manuell anlegen!)
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Häufige Fehler
|
||||||
|
|
||||||
|
| Fehler | Ursache | Lösung |
|
||||||
|
|---|---|---|
|
||||||
|
| `FileNotFoundException: Microsoft.Extensions.DependencyInjection.Abstractions` | Framework-Dependent Publish auf Server ohne .NET 8 | Self-Contained Publish verwenden |
|
||||||
|
| `HTTP 500.30` in IIS | App startet nicht | Application Pool auf `No Managed Code` setzen |
|
||||||
|
| `HTTP 500.30` + `sc-win32-status: 574` | App Pool falsch oder `AspNetCoreModuleV2` fehlt | Pool prüfen + Hosting Bundle installieren |
|
||||||
|
| App startet per `.exe`, aber nicht in IIS | `web.config` hat noch `processPath="dotnet"` | `web.config` auf `.\EnvelopeGenerator.Server.exe` korrigieren |
|
||||||
|
| Logs-Verzeichnis fehlt → App startet nicht | `stdoutLogEnabled="true"` aber `logs\` existiert nicht | `logs\`-Ordner anlegen + IIS_IUSRS Schreibrecht geben |
|
||||||
@@ -0,0 +1,13 @@
|
|||||||
|
{
|
||||||
|
"version": 1,
|
||||||
|
"isRoot": true,
|
||||||
|
"tools": {
|
||||||
|
"dotnet-ef": {
|
||||||
|
"version": "10.0.9",
|
||||||
|
"commands": [
|
||||||
|
"dotnet-ef"
|
||||||
|
],
|
||||||
|
"rollForward": false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -308,8 +308,8 @@ article {
|
|||||||
.home-btn-primary {
|
.home-btn-primary {
|
||||||
background: linear-gradient(135deg, #2c3e50 0%, #3498db 100%);
|
background: linear-gradient(135deg, #2c3e50 0%, #3498db 100%);
|
||||||
border: none;
|
border: none;
|
||||||
color: #fff;
|
color: #fff !important;
|
||||||
font-weight: 500;
|
font-weight: 700;
|
||||||
border-radius: 8px;
|
border-radius: 8px;
|
||||||
transition: filter 0.15s, box-shadow 0.15s;
|
transition: filter 0.15s, box-shadow 0.15s;
|
||||||
box-shadow: 0 2px 10px rgba(44,62,80,0.30);
|
box-shadow: 0 2px 10px rgba(44,62,80,0.30);
|
||||||
|
|||||||
@@ -52,8 +52,8 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.pdf-editor-wrapper {
|
.pdf-editor-wrapper {
|
||||||
position: relative;
|
width: 100%;
|
||||||
min-height: 100%;
|
height: 100%;
|
||||||
}
|
}
|
||||||
|
|
||||||
.sender-editor-pdf-viewer {
|
.sender-editor-pdf-viewer {
|
||||||
@@ -61,6 +61,14 @@
|
|||||||
height: 100%;
|
height: 100%;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.sender-editor-pdf-viewer .dxbl-toolbar {
|
||||||
|
justify-content: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sender-editor-pdf-viewer .dxbl-toolbar-left {
|
||||||
|
margin-inline: auto;
|
||||||
|
}
|
||||||
|
|
||||||
.sender-editor-pdf-viewer .dxbrv-document-surface {
|
.sender-editor-pdf-viewer .dxbrv-document-surface {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
|
|||||||
@@ -1,92 +1,95 @@
|
|||||||
window.envelopeEditor = {
|
window.envelopeEditor = {
|
||||||
_overlaySyncState: {},
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Returns click coordinates relative to the overlay element.
|
* Returns the click position normalised to [0,1] relative to the rendered PDF page
|
||||||
* @param {string} overlayId - The id of the overlay div
|
* element inside DxPdfViewer (or DxReportViewer as fallback).
|
||||||
* @param {number} clientX - MouseEventArgs.ClientX from Blazor
|
*
|
||||||
* @param {number} clientY - MouseEventArgs.ClientY from Blazor
|
* Normalising means the result is independent of zoom level: no matter how much the
|
||||||
* @returns {{ relX, relY, containerW, containerH }}
|
* user has zoomed in/out, the same physical spot on the PDF will always yield the same
|
||||||
|
* normalised value. C# multiplies by the page's point dimensions to get PDF points.
|
||||||
|
*
|
||||||
|
* @param {string} viewerCssClass - CssClass set on DxPdfViewer (e.g. "sender-editor-pdf-viewer")
|
||||||
|
* @param {number} clientX - MouseEvent.clientX from Blazor
|
||||||
|
* @param {number} clientY - MouseEvent.clientY from Blazor
|
||||||
|
* @returns {{ normX, normY, pageIndex } | null}
|
||||||
|
* normX / normY : 0..1 fraction within the page element
|
||||||
|
* pageIndex : 0-based index of the page the click landed on (-1 if not found)
|
||||||
*/
|
*/
|
||||||
getClickCoords: function (overlayId, clientX, clientY) {
|
getClickCoordsOnPdfPage: function (viewerCssClass, clientX, clientY) {
|
||||||
const el = document.getElementById(overlayId);
|
|
||||||
if (!el) return null;
|
// Find the viewer root element
|
||||||
|
const viewer = document.querySelector('.' + viewerCssClass);
|
||||||
|
if (!viewer) {
|
||||||
|
console.warn('[envelopeEditor] viewer not found for class:', viewerCssClass);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- Candidate page elements (ordered by preference) ---
|
||||||
|
// DxPdfViewer renders individual pages as .dxbl-pdfv-page elements.
|
||||||
|
// DxReportViewer uses .dxbrv-report-preview-content-img as fallback.
|
||||||
|
const pageSelectors = [
|
||||||
|
'.dxbl-pdfv-page',
|
||||||
|
'.dxbrv-report-preview-page',
|
||||||
|
'.dxbrv-report-preview-content-img',
|
||||||
|
];
|
||||||
|
|
||||||
|
let allPages = [];
|
||||||
|
for (const sel of pageSelectors) {
|
||||||
|
const found = Array.from(viewer.querySelectorAll(sel));
|
||||||
|
if (found.length > 0) {
|
||||||
|
allPages = found;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (allPages.length === 0) {
|
||||||
|
console.warn('[envelopeEditor] no page elements found inside viewer');
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- Find which page the click landed on ---
|
||||||
|
// Walk through all pages; pick the one whose bounding rect contains the click.
|
||||||
|
// If none contains it exactly, fall back to the page closest vertically.
|
||||||
|
let targetPage = null;
|
||||||
|
let targetIndex = -1;
|
||||||
|
let minDist = Infinity;
|
||||||
|
|
||||||
|
for (let i = 0; i < allPages.length; i++) {
|
||||||
|
const rect = allPages[i].getBoundingClientRect();
|
||||||
|
|
||||||
|
// Exact hit
|
||||||
|
if (clientX >= rect.left && clientX <= rect.right &&
|
||||||
|
clientY >= rect.top && clientY <= rect.bottom) {
|
||||||
|
targetPage = allPages[i];
|
||||||
|
targetIndex = i;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Track closest page (vertical centre distance) as fallback
|
||||||
|
const cy = rect.top + rect.height / 2;
|
||||||
|
const dist = Math.abs(clientY - cy);
|
||||||
|
if (dist < minDist) {
|
||||||
|
minDist = dist;
|
||||||
|
targetPage = allPages[i];
|
||||||
|
targetIndex = i;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!targetPage) return null;
|
||||||
|
|
||||||
|
const pageRect = targetPage.getBoundingClientRect();
|
||||||
|
|
||||||
|
// Clamp click inside page boundaries before normalising
|
||||||
|
const clampedX = Math.max(pageRect.left, Math.min(clientX, pageRect.right));
|
||||||
|
const clampedY = Math.max(pageRect.top, Math.min(clientY, pageRect.bottom));
|
||||||
|
|
||||||
|
const normX = (clampedX - pageRect.left) / pageRect.width;
|
||||||
|
const normY = (clampedY - pageRect.top) / pageRect.height;
|
||||||
|
|
||||||
const rect = el.getBoundingClientRect();
|
|
||||||
return {
|
return {
|
||||||
relX: clientX - rect.left,
|
normX: normX,
|
||||||
relY: clientY - rect.top,
|
normY: normY,
|
||||||
containerW: rect.width,
|
pageIndex: targetIndex
|
||||||
containerH: rect.height
|
|
||||||
};
|
};
|
||||||
},
|
|
||||||
|
|
||||||
syncOverlayToPage: function (wrapperId, overlayId) {
|
|
||||||
const wrapper = document.getElementById(wrapperId);
|
|
||||||
const overlay = document.getElementById(overlayId);
|
|
||||||
|
|
||||||
if (!wrapper || !overlay) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const existing = window.envelopeEditor._overlaySyncState[overlayId];
|
|
||||||
if (existing) {
|
|
||||||
return existing.sync();
|
|
||||||
}
|
|
||||||
|
|
||||||
const findTarget = (currentWrapper) => {
|
|
||||||
const page = currentWrapper.querySelector(".dxbrv-report-preview-content");
|
|
||||||
if (page) {
|
|
||||||
return page;
|
|
||||||
}
|
|
||||||
|
|
||||||
return currentWrapper.querySelector(".dxbrv-report-preview-content-img") ||
|
|
||||||
currentWrapper.querySelector("img.dxbrv-report-preview-content-img") ||
|
|
||||||
currentWrapper.querySelector(".dxbrv-document-surface img");
|
|
||||||
};
|
|
||||||
|
|
||||||
const sync = () => {
|
|
||||||
const currentWrapper = document.getElementById(wrapperId);
|
|
||||||
const currentOverlay = document.getElementById(overlayId);
|
|
||||||
|
|
||||||
if (!currentWrapper || !currentOverlay) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const target = findTarget(currentWrapper);
|
|
||||||
if (!target) {
|
|
||||||
currentOverlay.style.left = "0px";
|
|
||||||
currentOverlay.style.top = "0px";
|
|
||||||
currentOverlay.style.width = "0px";
|
|
||||||
currentOverlay.style.height = "0px";
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const wrapperRect = currentWrapper.getBoundingClientRect();
|
|
||||||
const targetRect = target.getBoundingClientRect();
|
|
||||||
|
|
||||||
currentOverlay.style.left = `${targetRect.left - wrapperRect.left + currentWrapper.scrollLeft}px`;
|
|
||||||
currentOverlay.style.top = `${targetRect.top - wrapperRect.top + currentWrapper.scrollTop}px`;
|
|
||||||
currentOverlay.style.width = `${targetRect.width}px`;
|
|
||||||
currentOverlay.style.height = `${targetRect.height}px`;
|
|
||||||
};
|
|
||||||
|
|
||||||
const scheduleSync = () => requestAnimationFrame(sync);
|
|
||||||
|
|
||||||
const observer = new MutationObserver(scheduleSync);
|
|
||||||
observer.observe(wrapper, { childList: true, subtree: true, attributes: true });
|
|
||||||
|
|
||||||
wrapper.addEventListener("scroll", scheduleSync, { passive: true });
|
|
||||||
window.addEventListener("resize", scheduleSync);
|
|
||||||
|
|
||||||
window.envelopeEditor._overlaySyncState[overlayId] = {
|
|
||||||
sync,
|
|
||||||
observer
|
|
||||||
};
|
|
||||||
|
|
||||||
sync();
|
|
||||||
setTimeout(sync, 50);
|
|
||||||
setTimeout(sync, 150);
|
|
||||||
setTimeout(sync, 400);
|
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|||||||
96
EnvelopeGenerator.Server/publish.bat
Normal file
96
EnvelopeGenerator.Server/publish.bat
Normal file
@@ -0,0 +1,96 @@
|
|||||||
|
@echo off
|
||||||
|
setlocal
|
||||||
|
|
||||||
|
echo ============================================================
|
||||||
|
echo EnvelopeGenerator.Server - Self-Contained Publish
|
||||||
|
echo Target: win-x64 / .NET 8 / Release
|
||||||
|
echo ============================================================
|
||||||
|
echo.
|
||||||
|
|
||||||
|
REM Must be run from the solution root directory.
|
||||||
|
REM This file is located under: EnvelopeGenerator.Server\
|
||||||
|
|
||||||
|
set PROJECT=EnvelopeGenerator.Server\EnvelopeGenerator.Server.csproj
|
||||||
|
set OUTPUT=publish-output
|
||||||
|
set RID=win-x64
|
||||||
|
set FRAMEWORK=net8.0
|
||||||
|
|
||||||
|
echo [1/3] Cleaning previous publish output...
|
||||||
|
if exist "%OUTPUT%" (
|
||||||
|
rmdir /s /q "%OUTPUT%"
|
||||||
|
echo Removed: %OUTPUT%
|
||||||
|
) else (
|
||||||
|
echo Nothing to clean.
|
||||||
|
)
|
||||||
|
echo.
|
||||||
|
|
||||||
|
echo [2/3] Publishing...
|
||||||
|
echo Project : %PROJECT%
|
||||||
|
echo Output : %OUTPUT%
|
||||||
|
echo RID : %RID%
|
||||||
|
echo Framework: %FRAMEWORK%
|
||||||
|
echo.
|
||||||
|
|
||||||
|
dotnet publish "%PROJECT%" ^
|
||||||
|
-c Release ^
|
||||||
|
-f %FRAMEWORK% ^
|
||||||
|
--self-contained true ^
|
||||||
|
--runtime %RID% ^
|
||||||
|
-o "%OUTPUT%"
|
||||||
|
|
||||||
|
if %ERRORLEVEL% neq 0 (
|
||||||
|
echo.
|
||||||
|
echo [ERROR] Publish failed! ERRORLEVEL=%ERRORLEVEL%
|
||||||
|
pause
|
||||||
|
exit /b %ERRORLEVEL%
|
||||||
|
)
|
||||||
|
|
||||||
|
echo.
|
||||||
|
echo [3/3] Verifying output...
|
||||||
|
|
||||||
|
set PASS=1
|
||||||
|
|
||||||
|
if not exist "%OUTPUT%\EnvelopeGenerator.Server.exe" (
|
||||||
|
echo [FAIL] EnvelopeGenerator.Server.exe not found!
|
||||||
|
set PASS=0
|
||||||
|
)
|
||||||
|
if not exist "%OUTPUT%\hostfxr.dll" (
|
||||||
|
echo [FAIL] hostfxr.dll not found! (Not a self-contained publish?)
|
||||||
|
set PASS=0
|
||||||
|
)
|
||||||
|
if not exist "%OUTPUT%\coreclr.dll" (
|
||||||
|
echo [FAIL] coreclr.dll not found! (Not a self-contained publish?)
|
||||||
|
set PASS=0
|
||||||
|
)
|
||||||
|
if not exist "%OUTPUT%\Microsoft.Extensions.DependencyInjection.Abstractions.dll" (
|
||||||
|
echo [FAIL] Microsoft.Extensions.DependencyInjection.Abstractions.dll not found!
|
||||||
|
set PASS=0
|
||||||
|
)
|
||||||
|
if not exist "%OUTPUT%\web.config" (
|
||||||
|
echo [FAIL] web.config not found!
|
||||||
|
set PASS=0
|
||||||
|
)
|
||||||
|
|
||||||
|
if "%PASS%"=="1" (
|
||||||
|
echo.
|
||||||
|
echo ============================================================
|
||||||
|
echo PUBLISH SUCCEEDED
|
||||||
|
echo Output folder: %~dp0%OUTPUT%
|
||||||
|
echo ============================================================
|
||||||
|
echo.
|
||||||
|
echo Next steps:
|
||||||
|
echo 1. Copy the contents of '%OUTPUT%' to the IIS application directory
|
||||||
|
echo 2. Set the IIS Application Pool to 'No Managed Code'
|
||||||
|
echo 3. Recycle the Application Pool
|
||||||
|
echo.
|
||||||
|
) else (
|
||||||
|
echo.
|
||||||
|
echo ============================================================
|
||||||
|
echo PUBLISH COMPLETED BUT VERIFICATION FAILED
|
||||||
|
echo Review the FAIL messages above.
|
||||||
|
echo ============================================================
|
||||||
|
echo.
|
||||||
|
)
|
||||||
|
|
||||||
|
pause
|
||||||
|
endlocal
|
||||||
@@ -45,6 +45,9 @@ EndProject
|
|||||||
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "EnvelopeGenerator.ReceiverUI", "EnvelopeGenerator.ReceiverUI\EnvelopeGenerator.ReceiverUI.csproj", "{FB2D306B-1042-4A70-31ED-F991A1599371}"
|
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "EnvelopeGenerator.ReceiverUI", "EnvelopeGenerator.ReceiverUI\EnvelopeGenerator.ReceiverUI.csproj", "{FB2D306B-1042-4A70-31ED-F991A1599371}"
|
||||||
EndProject
|
EndProject
|
||||||
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "EnvelopeGenerator.Server", "EnvelopeGenerator.Server", "{BF1700D5-592E-4FFA-84E8-5480E289A1F0}"
|
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "EnvelopeGenerator.Server", "EnvelopeGenerator.Server", "{BF1700D5-592E-4FFA-84E8-5480E289A1F0}"
|
||||||
|
ProjectSection(SolutionItems) = preProject
|
||||||
|
EnvelopeGenerator.Server\publish.bat = EnvelopeGenerator.Server\publish.bat
|
||||||
|
EndProjectSection
|
||||||
EndProject
|
EndProject
|
||||||
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "EnvelopeGenerator.Server", "EnvelopeGenerator.Server\EnvelopeGenerator.Server\EnvelopeGenerator.Server.csproj", "{4E6C54DA-576D-0955-2564-9EC890BB8279}"
|
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "EnvelopeGenerator.Server", "EnvelopeGenerator.Server\EnvelopeGenerator.Server\EnvelopeGenerator.Server.csproj", "{4E6C54DA-576D-0955-2564-9EC890BB8279}"
|
||||||
EndProject
|
EndProject
|
||||||
|
|||||||
Reference in New Issue
Block a user