Compare commits
50 Commits
feat/migr-
...
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 | |||
| 4d069cdaa0 | |||
| bbbfa4de01 | |||
| 3ff3373b27 | |||
| 2af8815cf6 | |||
| ca24a96084 | |||
| db368b889a | |||
| 8f451b9c2c | |||
| 6120e6062e | |||
| cc2aea90ed | |||
| 762a9e8bca | |||
| 6ed4caea4f | |||
| d94821433a | |||
| 278b9964f1 | |||
| e6722803bb | |||
| 47bc7675c9 | |||
| 789e312316 | |||
| 2a9bbb3fe5 | |||
| bc34317720 | |||
| 76ff3e47e1 | |||
| 2d22bfcd06 | |||
| 185c783824 | |||
| b957b4b4bb | |||
| df154d83cc | |||
| 49ec9fbead | |||
| 01fc29f59e | |||
| 733b70cca2 | |||
| 8f4b751303 |
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
|
||||||
|
|||||||
@@ -1,4 +1,6 @@
|
|||||||
namespace EnvelopeGenerator.Application.Common.Query;
|
using System.ComponentModel.DataAnnotations.Schema;
|
||||||
|
|
||||||
|
namespace EnvelopeGenerator.Application.Common.Query;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Stellt eine Abfrage dar, um die Details eines Empfängers zu lesen.
|
/// Stellt eine Abfrage dar, um die Details eines Empfängers zu lesen.
|
||||||
@@ -29,5 +31,6 @@ public record ReceiverQueryBase
|
|||||||
/// <see cref="Id"/>, <see cref="EmailAddress"/>, or <see cref="Signature"/> is not null.
|
/// <see cref="Id"/>, <see cref="EmailAddress"/>, or <see cref="Signature"/> is not null.
|
||||||
/// <para>Usage example: The query can be executed only if at least one criterion is specified.</para>
|
/// <para>Usage example: The query can be executed only if at least one criterion is specified.</para>
|
||||||
/// </remarks>
|
/// </remarks>
|
||||||
public bool HasAnyCriteria => Id is not null || EmailAddress is not null || Signature is not null;
|
[NotMapped]
|
||||||
|
public virtual bool HasAnyCriteria => Id is not null || EmailAddress is not null || Signature is not null;
|
||||||
}
|
}
|
||||||
@@ -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},
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ using EnvelopeGenerator.Application.Common.Query;
|
|||||||
using MediatR;
|
using MediatR;
|
||||||
using EnvelopeGenerator.Domain.Entities;
|
using EnvelopeGenerator.Domain.Entities;
|
||||||
using Microsoft.EntityFrameworkCore;
|
using Microsoft.EntityFrameworkCore;
|
||||||
|
using System.ComponentModel.DataAnnotations.Schema;
|
||||||
|
|
||||||
namespace EnvelopeGenerator.Application.Receivers.Queries;
|
namespace EnvelopeGenerator.Application.Receivers.Queries;
|
||||||
|
|
||||||
@@ -12,7 +13,24 @@ namespace EnvelopeGenerator.Application.Receivers.Queries;
|
|||||||
/// Stellt eine Abfrage dar, um die Details eines Empfängers zu lesen.
|
/// Stellt eine Abfrage dar, um die Details eines Empfängers zu lesen.
|
||||||
/// um spezifische Informationen über einen Empfänger abzurufen.
|
/// um spezifische Informationen über einen Empfänger abzurufen.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public record ReadReceiverQuery : ReceiverQueryBase, IRequest<IEnumerable<ReceiverDto>>;
|
public record ReadReceiverQuery : ReceiverQueryBase, IRequest<IEnumerable<ReceiverDto>>
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Suchbegriff für eine teilweise Übereinstimmung in der E-Mail Adresse des Empfängers
|
||||||
|
/// </summary>
|
||||||
|
public virtual string? EmailAddressSearch { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Checks whether any of the specified query criteria have a value.
|
||||||
|
/// </summary>
|
||||||
|
/// <remarks>
|
||||||
|
/// This property returns <c>true</c> if at least one of the fields
|
||||||
|
/// <see cref="ReceiverQueryBase.Id"/>, <see cref="ReceiverQueryBase.EmailAddress"/>, <see cref="EmailAddressSearch"/>, or <see cref="ReceiverQueryBase.Signature"/> is not null.
|
||||||
|
/// <para>Usage example: The query can be executed only if at least one criterion is specified.</para>
|
||||||
|
/// </remarks>
|
||||||
|
[NotMapped]
|
||||||
|
public override bool HasAnyCriteria => EmailAddressSearch is not null || base.HasAnyCriteria;
|
||||||
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
///
|
///
|
||||||
@@ -53,6 +71,11 @@ public class ReadReceiverQueryHandler : IRequestHandler<ReadReceiverQuery, IEnum
|
|||||||
query = query.Where(r => r.EmailAddress == email);
|
query = query.Where(r => r.EmailAddress == email);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (!string.IsNullOrWhiteSpace(request.EmailAddressSearch))
|
||||||
|
{
|
||||||
|
query = query.Where(r => EF.Functions.Like(r.EmailAddress, $"%{request.EmailAddressSearch}%"));
|
||||||
|
}
|
||||||
|
|
||||||
if (request.Signature is string signature)
|
if (request.Signature is string signature)
|
||||||
{
|
{
|
||||||
query = query.Where(r => r.Signature == signature);
|
query = query.Where(r => r.Signature == signature);
|
||||||
|
|||||||
@@ -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>
|
||||||
@@ -385,8 +389,7 @@
|
|||||||
|
|
||||||
void CreateEnvelope()
|
void CreateEnvelope()
|
||||||
{
|
{
|
||||||
// TODO: Navigate to envelope creation page
|
Navigation.NavigateTo("/sender/editor");
|
||||||
JSRuntime.InvokeVoidAsync("console.log", "Create envelope clicked - not yet implemented");
|
|
||||||
}
|
}
|
||||||
|
|
||||||
void EditEnvelope()
|
void EditEnvelope()
|
||||||
|
|||||||
@@ -1,5 +1,7 @@
|
|||||||
@page "/"
|
@page "/"
|
||||||
@inject IJSRuntime JS
|
@inject IJSRuntime JS
|
||||||
|
@inject NavigationManager Navigation
|
||||||
|
@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" />
|
||||||
|
|
||||||
@@ -25,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">
|
||||||
@@ -47,6 +60,7 @@
|
|||||||
PDF-Export
|
PDF-Export
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -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);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,723 @@
|
|||||||
|
@page "/envelope/{EnvelopeKey}"
|
||||||
|
@rendermode InteractiveServer
|
||||||
|
@using DevExpress.Blazor.Reporting
|
||||||
|
@using DevExpress.XtraReports.UI
|
||||||
|
@using EnvelopeGenerator.Server.Client.Models
|
||||||
|
@using EnvelopeGenerator.Server.Client.Models.Constants
|
||||||
|
@using EnvelopeGenerator.Server.Client.Services
|
||||||
|
@using EnvelopeGenerator.Application.Common.Dto.EnvelopeReceiver
|
||||||
|
@using Microsoft.JSInterop
|
||||||
|
@using DevExpress.Blazor
|
||||||
|
@using System.Drawing
|
||||||
|
@using System.Security.Claims
|
||||||
|
@using Microsoft.Extensions.Caching.Memory
|
||||||
|
@inject NavigationManager Navigation
|
||||||
|
@inject IJSRuntime JSRuntime
|
||||||
|
@inject EnvelopeGenerator.Server.Client.Services.AuthService AuthService
|
||||||
|
@inject EnvelopeGenerator.Server.Services.EnvelopeReceiverAuthorizationService ReceiverAuthorizationService
|
||||||
|
@inject EnvelopeGenerator.Server.Services.EnvelopeReceiverPageDataService PageDataService
|
||||||
|
@inject AppVersionService AppVersion
|
||||||
|
@inject IMemoryCache MemoryCache
|
||||||
|
@inject ILogger<ReceiverPage> Logger
|
||||||
|
@implements IDisposable
|
||||||
|
|
||||||
|
<link href="_content/DevExpress.Blazor.Themes/blazing-berry.bs5.min.css" rel="stylesheet" />
|
||||||
|
<link href="_content/DevExpress.Blazor.Reporting.Viewer/css/dx-blazor-reporting-components.bs5.css" rel="stylesheet" />
|
||||||
|
<link href="@AppVersion.GetVersionedUrl("css/envelope-viewer.css")" rel="stylesheet" />
|
||||||
|
<script src="@AppVersion.GetVersionedUrl("js/receiver-signature.js")"></script>
|
||||||
|
|
||||||
|
<div class="envelope-viewer-layout">
|
||||||
|
<div class="envelope-action-bar">
|
||||||
|
<div class="envelope-action-bar__inner" style="flex-direction: column; align-items: stretch; padding: 0.35rem 1.5rem; gap: 0.35rem;">
|
||||||
|
@* Row 1: Title + Sender + Badges *@
|
||||||
|
<div style="display: flex; align-items: center; justify-content: space-between; gap: 1rem;">
|
||||||
|
@* Left: Title + Sender *@
|
||||||
|
<div style="flex: 0 1 auto; min-width: 0; display: flex; align-items: center; gap: 0.75rem;">
|
||||||
|
@if (_envelopeReceiver is not null)
|
||||||
|
{
|
||||||
|
<div style="font-size: 0.9rem; font-weight: 600; color: #1f2937; white-space: nowrap; overflow: hidden; text-overflow: ellipsis;">
|
||||||
|
@(_envelopeReceiver.Envelope?.Title ?? "Dokument")
|
||||||
|
</div>
|
||||||
|
@if (!string.IsNullOrWhiteSpace(_envelopeReceiver.Envelope?.User?.FullName) || !string.IsNullOrWhiteSpace(_envelopeReceiver.Envelope?.User?.Email))
|
||||||
|
{
|
||||||
|
<span style="font-size: 0.7rem; color: #6b7280; white-space: nowrap;">
|
||||||
|
Von
|
||||||
|
@if (!string.IsNullOrWhiteSpace(_envelopeReceiver.Envelope?.User?.FullName))
|
||||||
|
{
|
||||||
|
<span style="font-weight: 500; color: #374151;">@_envelopeReceiver.Envelope.User.FullName</span>
|
||||||
|
}
|
||||||
|
@if (!string.IsNullOrWhiteSpace(_envelopeReceiver.Envelope?.User?.Email))
|
||||||
|
{
|
||||||
|
<span><@_envelopeReceiver.Envelope.User.Email></span>
|
||||||
|
}
|
||||||
|
@if (_envelopeReceiver.Envelope?.AddedWhen != null)
|
||||||
|
{
|
||||||
|
<span> · @_envelopeReceiver.Envelope.AddedWhen.ToString("dd.MM.yyyy")</span>
|
||||||
|
}
|
||||||
|
</span>
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
<div style="font-size: 0.9rem; font-weight: 600; color: #1f2937;">Dokumentenansicht</div>
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
@* Right: Badges + Signature status *@
|
||||||
|
<div class="d-flex align-items-center" style="gap: 0.75rem; flex: 0 0 auto;">
|
||||||
|
@if (_envelopeReceiver is not null)
|
||||||
|
{
|
||||||
|
<div class="d-flex flex-wrap align-items-center" style="gap: 0.3rem; font-size: 0.7rem;">
|
||||||
|
@if (!string.IsNullOrWhiteSpace(_envelopeReceiver.Name))
|
||||||
|
{
|
||||||
|
<span style="display: inline-flex; align-items: center; padding: 0.125rem 0.4rem; background: #f3f4f6; border-radius: 0.25rem; color: #374151; white-space: nowrap;">
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" width="9" height="9" fill="currentColor" class="me-1" viewBox="0 0 16 16">
|
||||||
|
<path d="M8 8a3 3 0 1 0 0-6 3 3 0 0 0 0 6Zm2-3a2 2 0 1 1-4 0 2 2 0 0 1 4 0Zm4 8c0 1-1 1-1 1H3s-1 0-1-1 1-4 6-4 6 3 6 4Z" />
|
||||||
|
</svg>
|
||||||
|
@_envelopeReceiver.Name
|
||||||
|
</span>
|
||||||
|
}
|
||||||
|
@if (_signatures.Count > 0)
|
||||||
|
{
|
||||||
|
<span style="display: inline-flex; align-items: center; padding: 0.125rem 0.4rem; background: @(_capturedSignature is not null ? "#d1fae5" : "#ede9fe"); border-radius: 0.25rem; color: @(_capturedSignature is not null ? "#065f46" : "#6d28d9"); font-weight: 500; white-space: nowrap;">
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" width="9" height="9" fill="currentColor" class="me-1" 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-10zM11.207 2.5 13.5 4.793 14.793 3.5 12.5 1.207 11.207 2.5zm1.586 3L10.5 3.207 4 9.707V10h.5a.5.5 0 0 1 .5.5v.5h.5a.5.5 0 0 1 .5.5v.5h.293l6.5-6.5zm-9.761 5.175-.106.106-1.528 3.821 3.821-1.528.106-.106A.5.5 0 0 1 5 12.5V12h-.5a.5.5 0 0 1-.5-.5V11h-.5a.5.5 0 0 1-.468-.325z" />
|
||||||
|
</svg>
|
||||||
|
@_signatures.Count Unterschrift@(_signatures.Count != 1 ? "en" : "")
|
||||||
|
@if (_capturedSignature is not null)
|
||||||
|
{
|
||||||
|
<span class="ms-1">✓</span>
|
||||||
|
}
|
||||||
|
</span>
|
||||||
|
}
|
||||||
|
@if (_envelopeReceiver.Envelope?.UseAccessCode ?? false)
|
||||||
|
{
|
||||||
|
<span style="display: inline-flex; align-items: center; padding: 0.125rem 0.4rem; background: #fef3c7; border-radius: 0.25rem; color: #92400e; font-weight: 500; white-space: nowrap;">
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" width="9" height="9" fill="currentColor" class="me-1" viewBox="0 0 16 16">
|
||||||
|
<path d="M8 1a2 2 0 0 1 2 2v4H6V3a2 2 0 0 1 2-2zm3 6V3a3 3 0 0 0-6 0v4a2 2 0 0 0-2 2v5a2 2 0 0 0 2 2h6a2 2 0 0 0 2-2V9a2 2 0 0 0-2-2z" />
|
||||||
|
</svg>
|
||||||
|
Code
|
||||||
|
</span>
|
||||||
|
}
|
||||||
|
@if (_envelopeReceiver.Envelope?.TFAEnabled ?? false)
|
||||||
|
{
|
||||||
|
<span style="display: inline-flex; align-items: center; padding: 0.125rem 0.4rem; background: #dbeafe; border-radius: 0.25rem; color: #1e40af; font-weight: 500; white-space: nowrap;">
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" width="9" height="9" fill="currentColor" class="me-1" viewBox="0 0 16 16">
|
||||||
|
<path d="M5.338 1.59a61.44 61.44 0 0 0-2.837.856.481.481 0 0 0-.328.39c-.554 4.157.726 7.19 2.253 9.188a10.725 10.725 0 0 0 2.287 2.233c.346.244.652.42.893.533.12.057.218.095.293.118a.55.55 0 0 0 .101.025.615.615 0 0 0 .1-.025c.076-.023.174-.061.294-.118.24-.113.547-.29.893-.533a10.726 10.726 0 0 0 2.287-2.233c1.527-1.997 2.807-5.031 2.253-9.188a.48.48 0 0 0-.328-.39c-.651-.213-1.75-.56-2.837-.855C9.552 1.29 8.531 1.067 8 1.067c-.53 0-1.552.223-2.662.524zM5.072.56C6.157.265 7.31 0 8 0s1.843.265 2.928.56c1.11.3 2.229.655 2.887.87a1.54 1.54 0 0 1 1.044 1.262c.596 4.477-.787 7.795-2.465 9.99a11.775 11.775 0 0 1-2.517 2.453 7.159 7.159 0 0 1-1.048.625c-.28.132-.581.24-.829.24s-.548-.108-.829-.24a7.158 7.158 0 0 1-1.048-.625 11.777 11.777 0 0 1-2.517-2.453C1.928 10.487.545 7.169 1.141 2.692A1.54 1.54 0 0 1 2.185 1.43 62.456 62.456 0 0 1 5.072.56z" />
|
||||||
|
<path d="M10.854 5.146a.5.5 0 0 1 0 .708l-3 3a.5.5 0 0 1-.708 0l-1.5-1.5a.5.5 0 1 1 .708-.708L7.5 7.793l2.646-2.647a.5.5 0 0 1 .708 0z" />
|
||||||
|
</svg>
|
||||||
|
2FA
|
||||||
|
</span>
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
|
||||||
|
@* Unterschreiben button — visible only when signature fields exist *@
|
||||||
|
@if (_signatures.Count > 0)
|
||||||
|
{
|
||||||
|
<button class="pdf-toolbar__btn pdf-toolbar__btn--signature-change pdf-toolbar__btn--signature-change-active"
|
||||||
|
@onclick="OpenSignaturePopup"
|
||||||
|
title="Unterschreiben"
|
||||||
|
style="flex-shrink: 0;">
|
||||||
|
<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-10zM11.207 2.5 13.5 4.793 14.793 3.5 12.5 1.207 11.207 2.5zm1.586 3L10.5 3.207 4 9.707V10h.5a.5.5 0 0 1 .5.5v.5h.5a.5.5 0 0 1 .5.5v.5h.293l6.5-6.5zm-9.761 5.175-.106.106-1.528 3.821 3.821-1.528.106-.106A.5.5 0 0 1 5 12.5V12h-.5a.5.5 0 0 1-.5-.5V11h-.5a.5.5 0 0 1-.468-.325z" />
|
||||||
|
</svg>
|
||||||
|
<span class="pdf-toolbar__btn-text">Unterschreiben</span>
|
||||||
|
</button>
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
@* Row 2: Messages *@
|
||||||
|
@if (_envelopeReceiver is not null && (!string.IsNullOrWhiteSpace(_envelopeReceiver.Envelope?.Message) || !string.IsNullOrWhiteSpace(_envelopeReceiver.PrivateMessage)))
|
||||||
|
{
|
||||||
|
<div style="display: flex; align-items: flex-start; gap: 0.5rem; font-size: 0.7rem; padding-top: 0.15rem; border-top: 1px solid #e5e7eb;">
|
||||||
|
@if (!string.IsNullOrWhiteSpace(_envelopeReceiver.Envelope?.Message))
|
||||||
|
{
|
||||||
|
<div style="flex: 1; min-width: 0; padding: 0.2rem 0.4rem; background: #f9fafb; border-radius: 0.25rem; border-left: 2px solid #9ca3af; display: flex; align-items: flex-start; gap: 0.25rem;">
|
||||||
|
<span style="font-weight: 500; color: #374151; flex-shrink: 0;">📧</span>
|
||||||
|
<span style="color: #6b7280; overflow: hidden; text-overflow: ellipsis; white-space: nowrap;">@_envelopeReceiver.Envelope.Message</span>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
@if (!string.IsNullOrWhiteSpace(_envelopeReceiver.PrivateMessage))
|
||||||
|
{
|
||||||
|
<div style="flex: 1; min-width: 0; padding: 0.2rem 0.4rem; background: #fef3c7; border-radius: 0.25rem; border-left: 2px solid #f59e0b; display: flex; align-items: flex-start; gap: 0.25rem;">
|
||||||
|
<span style="font-weight: 500; color: #92400e; flex-shrink: 0;">🔒</span>
|
||||||
|
<span style="color: #92400e; overflow: hidden; text-overflow: ellipsis; white-space: nowrap;">@_envelopeReceiver.PrivateMessage</span>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="envelope-content" style="padding: 0; overflow: hidden;">
|
||||||
|
@if (_isLoading)
|
||||||
|
{
|
||||||
|
<div class="d-flex justify-content-center align-items-center h-100">
|
||||||
|
<div class="text-center">
|
||||||
|
<div class="spinner-border text-white mb-3" style="width: 3.5rem; height: 3.5rem;" role="status">
|
||||||
|
<span class="visually-hidden">Lädt...</span>
|
||||||
|
</div>
|
||||||
|
<p class="text-white fw-semibold">Dokument wird geladen...</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
else if (_errorMessage is not null)
|
||||||
|
{
|
||||||
|
<div class="error-container">
|
||||||
|
<div class="alert alert-danger shadow-lg">
|
||||||
|
<div class="d-flex align-items-start">
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" width="28" height="28" fill="currentColor" class="me-3 flex-shrink-0" 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>
|
||||||
|
<h5 class="mb-2">Fehler beim Laden des Dokuments</h5>
|
||||||
|
<p class="mb-0">@_errorMessage</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
else if (_report is not null)
|
||||||
|
{
|
||||||
|
<DxReportViewer @ref="_reportViewer"
|
||||||
|
Report="_report"
|
||||||
|
RootCssClasses="w-100 h-100" />
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
@* Signature Popup *@
|
||||||
|
<DxPopup @bind-Visible="_signaturePopupVisible"
|
||||||
|
HeaderText="Unterschrift erstellen"
|
||||||
|
Width="620px"
|
||||||
|
MaxWidth="95vw"
|
||||||
|
ShowFooter="true"
|
||||||
|
CloseOnOutsideClick="false"
|
||||||
|
ShowCloseButton="false"
|
||||||
|
CloseOnEscape="false"
|
||||||
|
Shown="OnPopupShownAsync">
|
||||||
|
<BodyContentTemplate>
|
||||||
|
<ul class="nav nav-tabs mb-3" style="border-bottom: 2px solid #e9ecef;">
|
||||||
|
<li class="nav-item">
|
||||||
|
<button type="button"
|
||||||
|
class="nav-link @(_activeSignatureTab == SignatureTabDraw ? "active" : "")"
|
||||||
|
style="@(_activeSignatureTab == SignatureTabDraw ? "border-bottom: 3px solid #4F46E5; color: #4F46E5; font-weight: 600;" : "color: #6c757d;")"
|
||||||
|
@onclick="() => SetSignatureTabAsync(SignatureTabDraw)">
|
||||||
|
Zeichnen
|
||||||
|
</button>
|
||||||
|
</li>
|
||||||
|
<li class="nav-item">
|
||||||
|
<button type="button"
|
||||||
|
class="nav-link @(_activeSignatureTab == SignatureTabText ? "active" : "")"
|
||||||
|
style="@(_activeSignatureTab == SignatureTabText ? "border-bottom: 3px solid #4F46E5; color: #4F46E5; font-weight: 600;" : "color: #6c757d;")"
|
||||||
|
@onclick="() => SetSignatureTabAsync(SignatureTabText)">
|
||||||
|
Text
|
||||||
|
</button>
|
||||||
|
</li>
|
||||||
|
<li class="nav-item">
|
||||||
|
<button type="button"
|
||||||
|
class="nav-link @(_activeSignatureTab == SignatureTabImage ? "active" : "")"
|
||||||
|
style="@(_activeSignatureTab == SignatureTabImage ? "border-bottom: 3px solid #4F46E5; color: #4F46E5; font-weight: 600;" : "color: #6c757d;")"
|
||||||
|
@onclick="() => SetSignatureTabAsync(SignatureTabImage)">
|
||||||
|
Bild
|
||||||
|
</button>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
|
||||||
|
@if (_activeSignatureTab == SignatureTabDraw)
|
||||||
|
{
|
||||||
|
<p style="color: #6c757d; font-size: 0.875rem; margin-bottom: 0.75rem;">Bitte unterschreiben Sie im folgenden Feld.</p>
|
||||||
|
<canvas id="rp-signature-pad"
|
||||||
|
width="560"
|
||||||
|
height="180"
|
||||||
|
style="border: 2px solid #e9ecef; border-radius: 8px; background: white; width: 100%; max-width: 560px; touch-action: none; box-shadow: 0 1px 3px rgba(0,0,0,0.1);"></canvas>
|
||||||
|
}
|
||||||
|
else if (_activeSignatureTab == SignatureTabText)
|
||||||
|
{
|
||||||
|
<p style="color: #6c757d; font-size: 0.875rem; margin-bottom: 0.75rem;">Geben Sie Ihre Unterschrift als Text ein und wählen Sie eine Schriftart.</p>
|
||||||
|
<div class="row g-3 mb-3">
|
||||||
|
<div class="col-12 col-md-7">
|
||||||
|
<input class="form-control"
|
||||||
|
placeholder="Ihre Unterschrift"
|
||||||
|
value="@_typedSignatureText"
|
||||||
|
@oninput="OnTypedSignatureChanged"
|
||||||
|
style="border: 2px solid #e9ecef; border-radius: 6px; padding: 0.625rem;" />
|
||||||
|
</div>
|
||||||
|
<div class="col-12 col-md-5">
|
||||||
|
<select class="form-select"
|
||||||
|
value="@_typedSignatureFont"
|
||||||
|
@onchange="OnTypedSignatureFontChanged"
|
||||||
|
style="border: 2px solid #e9ecef; border-radius: 6px; padding: 0.625rem;">
|
||||||
|
@foreach (var font in TypedSignatureFonts)
|
||||||
|
{
|
||||||
|
<option value="@font.Value" style="font-family: @font.Value">@font.Text</option>
|
||||||
|
}
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<canvas id="rp-typed-signature-pad"
|
||||||
|
width="560"
|
||||||
|
height="180"
|
||||||
|
style="border: 2px solid #e9ecef; border-radius: 8px; background: white; width: 100%; max-width: 560px; box-shadow: 0 1px 3px rgba(0,0,0,0.1);"></canvas>
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
<p style="color: #6c757d; font-size: 0.875rem; margin-bottom: 0.75rem;">Laden Sie ein Bild Ihrer Unterschrift hoch.</p>
|
||||||
|
<input id="rp-signature-image-input"
|
||||||
|
class="form-control mb-3"
|
||||||
|
type="file"
|
||||||
|
accept="image/png,image/jpeg,image/webp"
|
||||||
|
style="border: 2px solid #e9ecef; border-radius: 6px; padding: 0.625rem;" />
|
||||||
|
<canvas id="rp-image-signature-pad"
|
||||||
|
width="560"
|
||||||
|
height="180"
|
||||||
|
style="border: 2px solid #e9ecef; border-radius: 8px; background: white; width: 100%; max-width: 560px; box-shadow: 0 1px 3px rgba(0,0,0,0.1);"></canvas>
|
||||||
|
}
|
||||||
|
|
||||||
|
<div style="border-top: 2px solid #e9ecef; margin-top: 1.5rem; padding-top: 1.5rem;">
|
||||||
|
<div class="row g-3">
|
||||||
|
<div class="col-12 col-md-6">
|
||||||
|
<label class="form-label" for="rp-signer-name" style="font-size: 0.875rem; font-weight: 500; color: #495057;">
|
||||||
|
Vor- und Nachname <span style="color: #dc3545;">*</span>
|
||||||
|
</label>
|
||||||
|
<input id="rp-signer-name"
|
||||||
|
class="form-control"
|
||||||
|
value="@_signerFullName"
|
||||||
|
@oninput="args => _signerFullName = args.Value?.ToString() ?? string.Empty"
|
||||||
|
style="border: 2px solid #e9ecef; border-radius: 6px; padding: 0.625rem;" />
|
||||||
|
</div>
|
||||||
|
<div class="col-12 col-md-6">
|
||||||
|
<label class="form-label" for="rp-signer-position" style="font-size: 0.875rem; font-weight: 500; color: #495057;">
|
||||||
|
Position <span style="color: #6c757d; font-weight: 400;">(optional)</span>
|
||||||
|
</label>
|
||||||
|
<input id="rp-signer-position"
|
||||||
|
class="form-control"
|
||||||
|
value="@_signerPosition"
|
||||||
|
@oninput="args => _signerPosition = args.Value?.ToString() ?? string.Empty"
|
||||||
|
style="border: 2px solid #e9ecef; border-radius: 6px; padding: 0.625rem;" />
|
||||||
|
</div>
|
||||||
|
<div class="col-12 col-md-6">
|
||||||
|
<label class="form-label" for="rp-signature-place" style="font-size: 0.875rem; font-weight: 500; color: #495057;">
|
||||||
|
Ort <span style="color: #dc3545;">*</span>
|
||||||
|
</label>
|
||||||
|
<input id="rp-signature-place"
|
||||||
|
class="form-control"
|
||||||
|
value="@_signaturePlace"
|
||||||
|
@oninput="args => _signaturePlace = args.Value?.ToString() ?? string.Empty"
|
||||||
|
style="border: 2px solid #e9ecef; border-radius: 6px; padding: 0.625rem;" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
@if (!string.IsNullOrWhiteSpace(_popupValidationMessage))
|
||||||
|
{
|
||||||
|
<div style="background: #fee; border-left: 4px solid #dc3545; padding: 0.75rem 1rem; margin-top: 1rem; border-radius: 4px;">
|
||||||
|
<span style="color: #dc3545; font-size: 0.875rem; font-weight: 500;">@_popupValidationMessage</span>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
</BodyContentTemplate>
|
||||||
|
<FooterContentTemplate>
|
||||||
|
<div class="d-flex gap-2 justify-content-between w-100" style="padding: 0.5rem 0;">
|
||||||
|
<button class="btn btn-outline-secondary"
|
||||||
|
@onclick="RenewSignatureAsync"
|
||||||
|
style="border-radius: 6px; padding: 0.625rem 1.25rem; font-weight: 500;">
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" width="14" height="14" fill="currentColor" class="me-1" viewBox="0 0 16 16">
|
||||||
|
<path fill-rule="evenodd" d="M8 3a5 5 0 1 0 4.546 2.914.5.5 0 0 1 .908-.417A6 6 0 1 1 8 2v1z" />
|
||||||
|
<path d="M8 4.466V.534a.25.25 0 0 1 .41-.192l2.36 1.966c.12.1.12.284 0 .384L8.41 4.658A.25.25 0 0 1 8 4.466z" />
|
||||||
|
</svg>
|
||||||
|
Erneuern
|
||||||
|
</button>
|
||||||
|
<button class="btn btn-primary"
|
||||||
|
@onclick="SaveSignatureAsync"
|
||||||
|
style="background: linear-gradient(135deg, #4F46E5 0%, #4338CA 100%); border: none; border-radius: 6px; padding: 0.625rem 2rem; font-weight: 600; box-shadow: 0 2px 4px rgba(79, 70, 229, 0.3);">
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" width="14" height="14" fill="currentColor" class="me-1" viewBox="0 0 16 16">
|
||||||
|
<path d="M13.854 3.646a.5.5 0 0 1 0 .708l-7 7a.5.5 0 0 1-.708 0l-3.5-3.5a.5.5 0 1 1 .708-.708L6.5 10.293l6.646-6.647a.5.5 0 0 1 .708 0z" />
|
||||||
|
</svg>
|
||||||
|
Speichern
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</FooterContentTemplate>
|
||||||
|
</DxPopup>
|
||||||
|
|
||||||
|
@code {
|
||||||
|
// ----- Constants -----
|
||||||
|
const string SignatureTabDraw = "draw";
|
||||||
|
const string SignatureTabText = "text";
|
||||||
|
const string SignatureTabImage = "image";
|
||||||
|
const string DrawCanvasId = "rp-signature-pad";
|
||||||
|
const string TypedCanvasId = "rp-typed-signature-pad";
|
||||||
|
const string ImageInputId = "rp-signature-image-input";
|
||||||
|
const string ImageCanvasId = "rp-image-signature-pad";
|
||||||
|
|
||||||
|
readonly (string Text, string Value)[] TypedSignatureFonts =
|
||||||
|
[
|
||||||
|
("Brush Script", "'Brush Script MT', cursive"),
|
||||||
|
("Segoe Script", "'Segoe Script', cursive"),
|
||||||
|
("Lucida Handwriting", "'Lucida Handwriting', cursive"),
|
||||||
|
("Comic Sans", "'Comic Sans MS', cursive"),
|
||||||
|
("Cursive", "cursive"),
|
||||||
|
];
|
||||||
|
|
||||||
|
// ----- Parameters -----
|
||||||
|
[Parameter] public string? EnvelopeKey { get; set; }
|
||||||
|
|
||||||
|
// ----- Page state -----
|
||||||
|
bool _isLoading = true;
|
||||||
|
string? _errorMessage;
|
||||||
|
byte[]? _pdfBytes;
|
||||||
|
IReadOnlyList<SignatureDto> _signatures = [];
|
||||||
|
EnvelopeGenerator.Application.Common.Dto.EnvelopeReceiver.EnvelopeReceiverDto? _envelopeReceiver;
|
||||||
|
ClaimsPrincipal? _receiverUser;
|
||||||
|
|
||||||
|
// ----- Report viewer -----
|
||||||
|
DxReportViewer? _reportViewer;
|
||||||
|
XtraReport? _report;
|
||||||
|
|
||||||
|
// ----- Signature popup state -----
|
||||||
|
SignatureCaptureDto? _capturedSignature;
|
||||||
|
bool _signaturePopupVisible = false;
|
||||||
|
string? _popupValidationMessage;
|
||||||
|
string _activeSignatureTab = SignatureTabDraw;
|
||||||
|
string _typedSignatureText = string.Empty;
|
||||||
|
string _typedSignatureFont = "'Brush Script MT', cursive";
|
||||||
|
string _signerFullName = string.Empty;
|
||||||
|
string _signerPosition = string.Empty;
|
||||||
|
string _signaturePlace = string.Empty;
|
||||||
|
|
||||||
|
// ----- Lifecycle -----
|
||||||
|
protected override async Task OnInitializedAsync()
|
||||||
|
{
|
||||||
|
if (string.IsNullOrWhiteSpace(EnvelopeKey))
|
||||||
|
{
|
||||||
|
_errorMessage = "Envelope-Schlüssel fehlt.";
|
||||||
|
_isLoading = false;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Authorization — same pattern as EnvelopeReceiverPage
|
||||||
|
_receiverUser = await ReceiverAuthorizationService.AuthorizeAsync(EnvelopeKey);
|
||||||
|
if (_receiverUser is null)
|
||||||
|
{
|
||||||
|
Navigation.NavigateTo($"/envelope/login/{Uri.EscapeDataString(EnvelopeKey)}");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
// Load PDF bytes via MediatR (uses authenticated user's claims)
|
||||||
|
_pdfBytes = await PageDataService.GetDocumentAsync(_receiverUser);
|
||||||
|
if (_pdfBytes is not { Length: > 0 })
|
||||||
|
{
|
||||||
|
_errorMessage = "Dokument konnte nicht geladen werden: Keine Daten empfangen.";
|
||||||
|
_isLoading = false;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Load signature fields for this receiver
|
||||||
|
_signatures = await PageDataService.GetSignaturesAsync(_receiverUser);
|
||||||
|
|
||||||
|
// Load envelope receiver metadata
|
||||||
|
_envelopeReceiver = await PageDataService.GetEnvelopeReceiverAsync(EnvelopeKey);
|
||||||
|
if (_envelopeReceiver is null)
|
||||||
|
Logger.LogWarning("Envelope receiver data is null for {EnvelopeKey}", EnvelopeKey);
|
||||||
|
|
||||||
|
// Build initial report (no signature image yet)
|
||||||
|
_report = BuildReport(_pdfBytes, _signatures, capturedSignature: null);
|
||||||
|
|
||||||
|
// Try to restore cached signature
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var cachedSignature = await PageDataService.GetCachedSignatureAsync(_receiverUser);
|
||||||
|
if (cachedSignature is not null)
|
||||||
|
{
|
||||||
|
_capturedSignature = cachedSignature;
|
||||||
|
_signerFullName = cachedSignature.FullName;
|
||||||
|
_signerPosition = cachedSignature.Position;
|
||||||
|
_signaturePlace = cachedSignature.Place;
|
||||||
|
_signaturePopupVisible = false;
|
||||||
|
|
||||||
|
// Rebuild with cached signature overlaid
|
||||||
|
_report = BuildReport(_pdfBytes, _signatures, _capturedSignature);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
_activeSignatureTab = SignatureTabDraw;
|
||||||
|
_signaturePopupVisible = false;
|
||||||
|
_popupValidationMessage = null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
Logger.LogWarning(ex, "Failed to load cached signature for {EnvelopeKey}", EnvelopeKey);
|
||||||
|
_activeSignatureTab = SignatureTabDraw;
|
||||||
|
_signaturePopupVisible = false;
|
||||||
|
_popupValidationMessage = null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
_errorMessage = $"Fehler beim Laden des Dokuments: {ex.Message}";
|
||||||
|
Logger.LogError(ex, "Unexpected error for {EnvelopeKey}", EnvelopeKey);
|
||||||
|
}
|
||||||
|
|
||||||
|
_isLoading = false;
|
||||||
|
await InvokeAsync(StateHasChanged);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ----- Report builder -----
|
||||||
|
/// <summary>
|
||||||
|
/// Builds an XtraReport wrapping the PDF bytes.
|
||||||
|
/// If a signature is captured and there are signature fields, the signature image is
|
||||||
|
/// first burned into the PDF via DevExpress PdfDocumentProcessor, then the modified
|
||||||
|
/// PDF is handed to XRPdfContent with GenerateOwnPages = true so that all pages appear.
|
||||||
|
/// </summary>
|
||||||
|
static XtraReport BuildReport(
|
||||||
|
byte[] pdfBytes,
|
||||||
|
IReadOnlyList<SignatureDto> signatures,
|
||||||
|
SignatureCaptureDto? capturedSignature)
|
||||||
|
{
|
||||||
|
// Always draw placeholder boxes on signature fields so the user knows where to sign.
|
||||||
|
// When a captured signature exists, it will be applied in the Signed page instead.
|
||||||
|
byte[] sourcePdf = pdfBytes;
|
||||||
|
if (signatures.Count > 0)
|
||||||
|
{
|
||||||
|
sourcePdf = DrawSignaturePlaceholders(pdfBytes, signatures);
|
||||||
|
}
|
||||||
|
|
||||||
|
var report = new XtraReport
|
||||||
|
{
|
||||||
|
PaperKind = DevExpress.Drawing.Printing.DXPaperKind.A4,
|
||||||
|
Landscape = false,
|
||||||
|
Margins = new System.Drawing.Printing.Margins(0, 0, 0, 0),
|
||||||
|
};
|
||||||
|
|
||||||
|
var detail = new DetailBand { HeightF = 0f };
|
||||||
|
report.Bands.Add(detail);
|
||||||
|
|
||||||
|
detail.Controls.Add(new XRPdfContent
|
||||||
|
{
|
||||||
|
Source = sourcePdf,
|
||||||
|
GenerateOwnPages = true,
|
||||||
|
});
|
||||||
|
|
||||||
|
return report;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Uses PdfSharp to draw a visible signature placeholder box on every signature field.
|
||||||
|
/// sig.X / sig.Y come from GetSignaturesAsync(UnitOfLength.Point) → already in PDF points.
|
||||||
|
/// PdfSharp coordinate origin: bottom-left, Y up. Conversion: pdfY = pageH - sigY - sigH
|
||||||
|
/// Signature field size (fixed): 1.77" × 1.96" = 127.44pt × 141.12pt
|
||||||
|
/// </summary>
|
||||||
|
static byte[] DrawSignaturePlaceholders(
|
||||||
|
byte[] pdfBytes,
|
||||||
|
IReadOnlyList<SignatureDto> signatures)
|
||||||
|
{
|
||||||
|
if (signatures.Count == 0) return pdfBytes;
|
||||||
|
|
||||||
|
using var inputMs = new System.IO.MemoryStream(pdfBytes);
|
||||||
|
using var outputMs = new System.IO.MemoryStream();
|
||||||
|
|
||||||
|
var document = PdfSharp.Pdf.IO.PdfReader.Open(
|
||||||
|
inputMs,
|
||||||
|
PdfSharp.Pdf.IO.PdfDocumentOpenMode.Modify);
|
||||||
|
|
||||||
|
const double sigW = 1.77 * 72; // 127.44 pt
|
||||||
|
const double sigH = 1.96 * 72; // 141.12 pt
|
||||||
|
|
||||||
|
foreach (var sig in signatures)
|
||||||
|
{
|
||||||
|
int pageIndex = sig.Page - 1;
|
||||||
|
if (pageIndex < 0 || pageIndex >= document.PageCount) continue;
|
||||||
|
|
||||||
|
var page = document.Pages[pageIndex];
|
||||||
|
|
||||||
|
// PdfSharp XGraphics uses top-left origin, Y down — same as sig.X/sig.Y
|
||||||
|
// No coordinate conversion needed.
|
||||||
|
using var gfx = PdfSharp.Drawing.XGraphics.FromPdfPage(page);
|
||||||
|
|
||||||
|
var rect = new PdfSharp.Drawing.XRect(sig.X, sig.Y, sigW, sigH);
|
||||||
|
|
||||||
|
// Filled semi-transparent rectangle
|
||||||
|
var fillBrush = new PdfSharp.Drawing.XSolidBrush(
|
||||||
|
PdfSharp.Drawing.XColor.FromArgb(40, 60, 80, 160));
|
||||||
|
var borderPen = new PdfSharp.Drawing.XPen(
|
||||||
|
PdfSharp.Drawing.XColor.FromArgb(200, 60, 80, 200), 1.5);
|
||||||
|
|
||||||
|
gfx.DrawRectangle(fillBrush, rect);
|
||||||
|
gfx.DrawRectangle(borderPen, rect);
|
||||||
|
|
||||||
|
// "UNTERSCHRIFT" label centred in the box
|
||||||
|
var font = new PdfSharp.Drawing.XFont("Arial", 9,
|
||||||
|
PdfSharp.Drawing.XFontStyleEx.Bold);
|
||||||
|
var textBrush = new PdfSharp.Drawing.XSolidBrush(
|
||||||
|
PdfSharp.Drawing.XColor.FromArgb(200, 40, 60, 140));
|
||||||
|
|
||||||
|
var textFmt = new PdfSharp.Drawing.XStringFormat
|
||||||
|
{
|
||||||
|
Alignment = PdfSharp.Drawing.XStringAlignment.Center,
|
||||||
|
LineAlignment = PdfSharp.Drawing.XLineAlignment.Center,
|
||||||
|
};
|
||||||
|
gfx.DrawString("UNTERSCHRIFT", font, textBrush, rect, textFmt);
|
||||||
|
}
|
||||||
|
|
||||||
|
document.Save(outputMs);
|
||||||
|
return outputMs.ToArray();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>Converts a base64 data URL (data:image/...;base64,...) to raw bytes.</summary>
|
||||||
|
static byte[]? DataUrlToBytes(string dataUrl)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var commaIndex = dataUrl.IndexOf(',');
|
||||||
|
if (commaIndex < 0) return null;
|
||||||
|
return Convert.FromBase64String(dataUrl[(commaIndex + 1)..]);
|
||||||
|
}
|
||||||
|
catch
|
||||||
|
{
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ----- Signature popup handlers -----
|
||||||
|
void OpenSignaturePopup()
|
||||||
|
{
|
||||||
|
_activeSignatureTab = SignatureTabDraw;
|
||||||
|
_signaturePopupVisible = true;
|
||||||
|
_popupValidationMessage = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
async Task OnPopupShownAsync()
|
||||||
|
{
|
||||||
|
await InitializeActiveSignatureTabAsync();
|
||||||
|
}
|
||||||
|
|
||||||
|
async Task SetSignatureTabAsync(string tab)
|
||||||
|
{
|
||||||
|
_activeSignatureTab = tab;
|
||||||
|
_popupValidationMessage = null;
|
||||||
|
await InvokeAsync(StateHasChanged);
|
||||||
|
await Task.Delay(50);
|
||||||
|
await InitializeActiveSignatureTabAsync();
|
||||||
|
}
|
||||||
|
|
||||||
|
async Task InitializeActiveSignatureTabAsync()
|
||||||
|
{
|
||||||
|
if (_activeSignatureTab == SignatureTabDraw)
|
||||||
|
await JSRuntime.InvokeVoidAsync("receiverSignature.initialize", DrawCanvasId);
|
||||||
|
else if (_activeSignatureTab == SignatureTabText)
|
||||||
|
{
|
||||||
|
await JSRuntime.InvokeVoidAsync("receiverSignature.initializeTyped", TypedCanvasId);
|
||||||
|
await RenderTypedSignatureAsync();
|
||||||
|
}
|
||||||
|
else
|
||||||
|
await JSRuntime.InvokeVoidAsync("receiverSignature.initializeImage", ImageInputId, ImageCanvasId);
|
||||||
|
}
|
||||||
|
|
||||||
|
async Task RenewSignatureAsync()
|
||||||
|
{
|
||||||
|
_popupValidationMessage = null;
|
||||||
|
if (_activeSignatureTab == SignatureTabDraw)
|
||||||
|
await JSRuntime.InvokeVoidAsync("receiverSignature.clear", DrawCanvasId);
|
||||||
|
else if (_activeSignatureTab == SignatureTabText)
|
||||||
|
{
|
||||||
|
_typedSignatureText = string.Empty;
|
||||||
|
await JSRuntime.InvokeVoidAsync("receiverSignature.clearTyped", TypedCanvasId);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
await JSRuntime.InvokeVoidAsync("receiverSignature.clearImage", ImageInputId, ImageCanvasId);
|
||||||
|
}
|
||||||
|
|
||||||
|
async Task OnTypedSignatureChanged(Microsoft.AspNetCore.Components.ChangeEventArgs args)
|
||||||
|
{
|
||||||
|
_typedSignatureText = args.Value?.ToString() ?? string.Empty;
|
||||||
|
await RenderTypedSignatureAsync();
|
||||||
|
}
|
||||||
|
|
||||||
|
async Task OnTypedSignatureFontChanged(Microsoft.AspNetCore.Components.ChangeEventArgs args)
|
||||||
|
{
|
||||||
|
_typedSignatureFont = args.Value?.ToString() ?? _typedSignatureFont;
|
||||||
|
await RenderTypedSignatureAsync();
|
||||||
|
}
|
||||||
|
|
||||||
|
async Task RenderTypedSignatureAsync()
|
||||||
|
{
|
||||||
|
await JSRuntime.InvokeVoidAsync("receiverSignature.renderTypedSignature",
|
||||||
|
TypedCanvasId, _typedSignatureText, _typedSignatureFont);
|
||||||
|
}
|
||||||
|
|
||||||
|
async Task SaveSignatureAsync()
|
||||||
|
{
|
||||||
|
if (string.IsNullOrWhiteSpace(_signerFullName))
|
||||||
|
{
|
||||||
|
_popupValidationMessage = "Bitte geben Sie Vor- und Nachname ein.";
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (string.IsNullOrWhiteSpace(_signaturePlace))
|
||||||
|
{
|
||||||
|
_popupValidationMessage = "Bitte geben Sie den Ort ein.";
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
var signatureDataUrl = await GetActiveSignatureDataUrlAsync();
|
||||||
|
if (string.IsNullOrWhiteSpace(signatureDataUrl))
|
||||||
|
{
|
||||||
|
_popupValidationMessage = "Die Unterschrift ist erforderlich.";
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
_popupValidationMessage = null;
|
||||||
|
_capturedSignature = new SignatureCaptureDto
|
||||||
|
{
|
||||||
|
DataUrl = signatureDataUrl,
|
||||||
|
FullName = _signerFullName.Trim(),
|
||||||
|
Position = _signerPosition.Trim(),
|
||||||
|
Place = _signaturePlace.Trim(),
|
||||||
|
};
|
||||||
|
_signaturePopupVisible = false;
|
||||||
|
|
||||||
|
// Store signature in IMemoryCache with a Guid key (1 minute TTL)
|
||||||
|
var sid = Guid.NewGuid().ToString("N");
|
||||||
|
MemoryCache.Set(
|
||||||
|
sid,
|
||||||
|
_capturedSignature,
|
||||||
|
TimeSpan.FromMinutes(1));
|
||||||
|
|
||||||
|
Logger.LogInformation(
|
||||||
|
"Signature cached with sid={Sid} for envelope {EnvelopeKey}", sid, EnvelopeKey);
|
||||||
|
|
||||||
|
// Null the report → DxReportViewer removed from DOM → no crash on dispose
|
||||||
|
_report = null;
|
||||||
|
await InvokeAsync(StateHasChanged);
|
||||||
|
await Task.Delay(50);
|
||||||
|
|
||||||
|
// Navigate — forceLoad:true for clean circuit teardown
|
||||||
|
Navigation.NavigateTo(
|
||||||
|
$"/envelope/{Uri.EscapeDataString(EnvelopeKey!)}/signed?sid={sid}",
|
||||||
|
forceLoad: true);
|
||||||
|
}
|
||||||
|
|
||||||
|
async Task<string?> GetActiveSignatureDataUrlAsync()
|
||||||
|
{
|
||||||
|
if (_activeSignatureTab == SignatureTabDraw)
|
||||||
|
return await JSRuntime.InvokeAsync<string?>("receiverSignature.getDataUrl", DrawCanvasId);
|
||||||
|
|
||||||
|
if (_activeSignatureTab == SignatureTabText)
|
||||||
|
{
|
||||||
|
await RenderTypedSignatureAsync();
|
||||||
|
return await JSRuntime.InvokeAsync<string?>("receiverSignature.getTypedDataUrl", TypedCanvasId);
|
||||||
|
}
|
||||||
|
|
||||||
|
return await JSRuntime.InvokeAsync<string?>("receiverSignature.getImageDataUrl", ImageCanvasId);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ----- Disposal -----
|
||||||
|
public void Dispose()
|
||||||
|
{
|
||||||
|
_report?.Dispose();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,396 @@
|
|||||||
|
@page "/envelope/{EnvelopeKey}/signed"
|
||||||
|
@rendermode InteractiveServer
|
||||||
|
@using DevExpress.Blazor.Reporting
|
||||||
|
@using DevExpress.XtraReports.UI
|
||||||
|
@using EnvelopeGenerator.Server.Client.Models
|
||||||
|
@using EnvelopeGenerator.Server.Client.Services
|
||||||
|
@using EnvelopeGenerator.Application.Common.Dto.EnvelopeReceiver
|
||||||
|
@using Microsoft.Extensions.Caching.Memory
|
||||||
|
@using System.Security.Claims
|
||||||
|
@inject NavigationManager Navigation
|
||||||
|
@inject IJSRuntime JSRuntime
|
||||||
|
@inject EnvelopeGenerator.Server.Client.Services.AuthService AuthService
|
||||||
|
@inject EnvelopeGenerator.Server.Services.EnvelopeReceiverAuthorizationService ReceiverAuthorizationService
|
||||||
|
@inject EnvelopeGenerator.Server.Services.EnvelopeReceiverPageDataService PageDataService
|
||||||
|
@inject AppVersionService AppVersion
|
||||||
|
@inject IMemoryCache MemoryCache
|
||||||
|
@inject ILogger<ReceiverSignedPage> Logger
|
||||||
|
@implements IDisposable
|
||||||
|
|
||||||
|
<link href="_content/DevExpress.Blazor.Themes/blazing-berry.bs5.min.css" rel="stylesheet" />
|
||||||
|
<link href="_content/DevExpress.Blazor.Reporting.Viewer/css/dx-blazor-reporting-components.bs5.css" rel="stylesheet" />
|
||||||
|
<link href="@AppVersion.GetVersionedUrl("css/envelope-viewer.css")" rel="stylesheet" />
|
||||||
|
|
||||||
|
<div class="envelope-viewer-layout">
|
||||||
|
<div class="envelope-action-bar">
|
||||||
|
<div class="envelope-action-bar__inner" style="flex-direction: column; align-items: stretch; padding: 0.35rem 1.5rem; gap: 0.35rem;">
|
||||||
|
<div style="display: flex; align-items: center; justify-content: space-between; gap: 1rem;">
|
||||||
|
<div style="flex: 0 1 auto; min-width: 0; display: flex; align-items: center; gap: 0.75rem;">
|
||||||
|
@if (_envelopeReceiver is not null)
|
||||||
|
{
|
||||||
|
<div style="font-size: 0.9rem; font-weight: 600; color: #1f2937; white-space: nowrap; overflow: hidden; text-overflow: ellipsis;">
|
||||||
|
@(_envelopeReceiver.Envelope?.Title ?? "Dokument")
|
||||||
|
</div>
|
||||||
|
@if (!string.IsNullOrWhiteSpace(_envelopeReceiver.Envelope?.User?.FullName))
|
||||||
|
{
|
||||||
|
<span style="font-size: 0.7rem; color: #6b7280; white-space: nowrap;">
|
||||||
|
Von <span style="font-weight: 500; color: #374151;">@_envelopeReceiver.Envelope.User.FullName</span>
|
||||||
|
</span>
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
<div style="font-size: 0.9rem; font-weight: 600; color: #1f2937;">Signiertes Dokument</div>
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
@* Right: Submit button *@
|
||||||
|
<div style="flex: 0 0 auto;">
|
||||||
|
<button class="pdf-toolbar__btn pdf-toolbar__btn--signature-change pdf-toolbar__btn--signature-change-active"
|
||||||
|
@onclick="OpenSubmitConfirmPopup"
|
||||||
|
disabled="@_isLoggingOut"
|
||||||
|
title="Abschließen"
|
||||||
|
style="flex-shrink: 0;">
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" width="14" height="14" fill="currentColor" 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>
|
||||||
|
<span class="pdf-toolbar__btn-text">Abschließen</span>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="envelope-content" style="padding: 0; overflow: hidden;">
|
||||||
|
@if (_isLoading)
|
||||||
|
{
|
||||||
|
<div class="d-flex justify-content-center align-items-center h-100">
|
||||||
|
<div class="text-center">
|
||||||
|
<div class="spinner-border text-white mb-3" style="width: 3.5rem; height: 3.5rem;" role="status">
|
||||||
|
<span class="visually-hidden">Lädt...</span>
|
||||||
|
</div>
|
||||||
|
<p class="text-white fw-semibold">Dokument wird geladen...</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
else if (_errorMessage is not null)
|
||||||
|
{
|
||||||
|
<div class="error-container">
|
||||||
|
<div class="alert alert-danger shadow-lg">
|
||||||
|
<div class="d-flex align-items-start">
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" width="28" height="28" fill="currentColor" class="me-3 flex-shrink-0" 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>
|
||||||
|
<h5 class="mb-2">Fehler</h5>
|
||||||
|
<p class="mb-0">@_errorMessage</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
else if (_report is not null)
|
||||||
|
{
|
||||||
|
<DxReportViewer Report="_report" RootCssClasses="w-100 h-100" />
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
@* Submit confirmation popup *@
|
||||||
|
<DxPopup @bind-Visible="_submitConfirmVisible"
|
||||||
|
HeaderText="Unterschrift bestätigen"
|
||||||
|
Width="440px"
|
||||||
|
MaxWidth="95vw"
|
||||||
|
ShowFooter="true"
|
||||||
|
CloseOnOutsideClick="false"
|
||||||
|
ShowCloseButton="false"
|
||||||
|
CloseOnEscape="false">
|
||||||
|
<BodyContentTemplate>
|
||||||
|
<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;">
|
||||||
|
Möchten Sie das Dokument verbindlich unterschreiben?
|
||||||
|
</p>
|
||||||
|
<p style="margin: 0; color: #6b7280; font-size: 0.85rem; line-height: 1.5;">
|
||||||
|
Diese Aktion kann nicht rückgängig gemacht werden. Mit der Bestätigung erklären Sie, das oben angezeigte Dokument elektronisch unterzeichnet zu haben. Das unterzeichnete Dokument wird anschließend an alle beteiligten Parteien übermittelt.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</BodyContentTemplate>
|
||||||
|
<FooterContentTemplate>
|
||||||
|
<div class="d-flex gap-2 justify-content-end w-100" style="padding: 0.5rem 0;">
|
||||||
|
<button class="btn btn-outline-secondary"
|
||||||
|
@onclick="() => _submitConfirmVisible = false"
|
||||||
|
style="border-radius: 6px; padding: 0.5rem 1.25rem; font-weight: 500;">
|
||||||
|
Abbrechen
|
||||||
|
</button>
|
||||||
|
<button class="btn btn-primary"
|
||||||
|
@onclick="SubmitAndLogoutAsync"
|
||||||
|
disabled="@_isLoggingOut"
|
||||||
|
style="background: linear-gradient(135deg, #059669 0%, #047857 100%); border: none; border-radius: 6px; padding: 0.5rem 1.5rem; font-weight: 600; box-shadow: 0 2px 4px rgba(5, 150, 105, 0.3);">
|
||||||
|
@if (_isLoggingOut)
|
||||||
|
{
|
||||||
|
<span class="spinner-border spinner-border-sm me-1" role="status"></span>
|
||||||
|
}
|
||||||
|
Abschließen
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</FooterContentTemplate>
|
||||||
|
</DxPopup>
|
||||||
|
|
||||||
|
@code {
|
||||||
|
[Parameter] public string? EnvelopeKey { get; set; }
|
||||||
|
|
||||||
|
[SupplyParameterFromQuery(Name = "sid")]
|
||||||
|
public string? Sid { get; set; }
|
||||||
|
|
||||||
|
bool _isLoading = true;
|
||||||
|
string? _errorMessage;
|
||||||
|
ClaimsPrincipal? _receiverUser;
|
||||||
|
EnvelopeGenerator.Application.Common.Dto.EnvelopeReceiver.EnvelopeReceiverDto? _envelopeReceiver;
|
||||||
|
IReadOnlyList<SignatureDto> _signatures = [];
|
||||||
|
XtraReport? _report;
|
||||||
|
SignatureCaptureDto? _sig;
|
||||||
|
|
||||||
|
// ----- Submit / logout state -----
|
||||||
|
bool _isLoggingOut = false;
|
||||||
|
bool _submitConfirmVisible = false;
|
||||||
|
|
||||||
|
void OpenSubmitConfirmPopup() => _submitConfirmVisible = true;
|
||||||
|
|
||||||
|
async Task SubmitAndLogoutAsync()
|
||||||
|
{
|
||||||
|
if (_isLoggingOut) return;
|
||||||
|
_isLoggingOut = true;
|
||||||
|
_submitConfirmVisible = false;
|
||||||
|
await InvokeAsync(StateHasChanged);
|
||||||
|
await AuthService.LogoutEnvelopeReceiverAsync(EnvelopeKey!);
|
||||||
|
Navigation.NavigateTo("/", forceLoad: true);
|
||||||
|
}
|
||||||
|
|
||||||
|
protected override async Task OnInitializedAsync()
|
||||||
|
{
|
||||||
|
if (string.IsNullOrWhiteSpace(EnvelopeKey))
|
||||||
|
{
|
||||||
|
_errorMessage = "Envelope-Schlüssel fehlt.";
|
||||||
|
_isLoading = false;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
_receiverUser = await ReceiverAuthorizationService.AuthorizeAsync(EnvelopeKey);
|
||||||
|
if (_receiverUser is null)
|
||||||
|
{
|
||||||
|
Navigation.NavigateTo($"/envelope/login/{Uri.EscapeDataString(EnvelopeKey)}");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Read signature from IMemoryCache
|
||||||
|
if (!string.IsNullOrWhiteSpace(Sid)
|
||||||
|
&& MemoryCache.TryGetValue(Sid, out SignatureCaptureDto? cached)
|
||||||
|
&& cached is not null)
|
||||||
|
{
|
||||||
|
_sig = cached;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Cache miss or missing sid — redirect back to report page
|
||||||
|
if (_sig is null)
|
||||||
|
{
|
||||||
|
Logger.LogWarning(
|
||||||
|
"[SignedPage] Cache miss or no sid={Sid} for {EnvelopeKey}, redirecting to report page.",
|
||||||
|
Sid, EnvelopeKey);
|
||||||
|
Navigation.NavigateTo(
|
||||||
|
$"/envelope/{Uri.EscapeDataString(EnvelopeKey)}",
|
||||||
|
forceLoad: true);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var pdfBytes = await PageDataService.GetDocumentAsync(_receiverUser);
|
||||||
|
if (pdfBytes is not { Length: > 0 })
|
||||||
|
{
|
||||||
|
_errorMessage = "Dokument konnte nicht geladen werden.";
|
||||||
|
_isLoading = false;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
_envelopeReceiver = await PageDataService.GetEnvelopeReceiverAsync(EnvelopeKey);
|
||||||
|
_signatures = await PageDataService.GetSignaturesAsync(_receiverUser);
|
||||||
|
|
||||||
|
// Burn signature image + info onto PDF via PdfSharp
|
||||||
|
if (_sig is not null && _signatures.Count > 0)
|
||||||
|
pdfBytes = DrawSignaturesOnPdf(pdfBytes, _signatures, _sig);
|
||||||
|
|
||||||
|
var report = new XtraReport
|
||||||
|
{
|
||||||
|
PaperKind = DevExpress.Drawing.Printing.DXPaperKind.A4,
|
||||||
|
Landscape = false,
|
||||||
|
Margins = new System.Drawing.Printing.Margins(0, 0, 0, 0),
|
||||||
|
};
|
||||||
|
var detail = new DetailBand();
|
||||||
|
report.Bands.Add(detail);
|
||||||
|
detail.Controls.Add(new XRPdfContent
|
||||||
|
{
|
||||||
|
Source = pdfBytes,
|
||||||
|
GenerateOwnPages = true,
|
||||||
|
});
|
||||||
|
_report = report;
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
_errorMessage = $"Fehler: {ex.Message}";
|
||||||
|
Logger.LogError(ex, "Error loading signed page for {EnvelopeKey}", EnvelopeKey);
|
||||||
|
}
|
||||||
|
|
||||||
|
_isLoading = false;
|
||||||
|
await InvokeAsync(StateHasChanged);
|
||||||
|
}
|
||||||
|
|
||||||
|
protected override async Task OnAfterRenderAsync(bool firstRender)
|
||||||
|
{
|
||||||
|
if (!firstRender) return;
|
||||||
|
|
||||||
|
if (_sig is not null)
|
||||||
|
{
|
||||||
|
await JSRuntime.InvokeVoidAsync("console.log",
|
||||||
|
$"[SignedPage] sid={Sid} | FullName={_sig.FullName} | Place={_sig.Place} | Position={_sig.Position} | DataUrl.Length={_sig.DataUrl?.Length ?? 0}");
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
await JSRuntime.InvokeVoidAsync("console.log",
|
||||||
|
$"[SignedPage] Cache miss or no sid. sid={Sid}");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public void Dispose()
|
||||||
|
{
|
||||||
|
_report?.Dispose();
|
||||||
|
}
|
||||||
|
|
||||||
|
// ----- PDF signature rendering -----
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Uses PdfSharp to burn the captured signature onto the PDF at each signature field.
|
||||||
|
/// Layout per field (top-left origin, Y down, units = PDF points):
|
||||||
|
/// [top 65%] signature image
|
||||||
|
/// [separator line]
|
||||||
|
/// [bottom 35%] FullName (bold) / Position (optional) / Place, Date
|
||||||
|
/// </summary>
|
||||||
|
static byte[] DrawSignaturesOnPdf(
|
||||||
|
byte[] pdfBytes,
|
||||||
|
IReadOnlyList<SignatureDto> signatures,
|
||||||
|
SignatureCaptureDto sig)
|
||||||
|
{
|
||||||
|
var imgBytes = DataUrlToBytes(sig.DataUrl);
|
||||||
|
if (imgBytes is not { Length: > 0 }) return pdfBytes;
|
||||||
|
|
||||||
|
using var inputMs = new System.IO.MemoryStream(pdfBytes);
|
||||||
|
using var outputMs = new System.IO.MemoryStream();
|
||||||
|
|
||||||
|
var document = PdfSharp.Pdf.IO.PdfReader.Open(
|
||||||
|
inputMs, PdfSharp.Pdf.IO.PdfDocumentOpenMode.Modify);
|
||||||
|
|
||||||
|
const double sigW = 1.77 * 72; // 127.44 pt
|
||||||
|
const double sigH = 1.96 * 72; // 141.12 pt
|
||||||
|
const double imgRatio = 0.52; // top 52% = image
|
||||||
|
const double lineH = 11.5; // fixed row height matching font size (bold 7.5pt + normal 6.5pt)
|
||||||
|
const double bgPad = 3.0; // background box padding around content (pt)
|
||||||
|
|
||||||
|
var black = PdfSharp.Drawing.XColor.FromArgb(255, 20, 20, 20);
|
||||||
|
var darkGray = PdfSharp.Drawing.XColor.FromArgb(255, 80, 80, 80);
|
||||||
|
var lineColor = PdfSharp.Drawing.XColor.FromArgb(180, 100, 100, 120);
|
||||||
|
|
||||||
|
var bgColor = PdfSharp.Drawing.XColor.FromArgb(255, 255, 253, 240);
|
||||||
|
var bgBrush = new PdfSharp.Drawing.XSolidBrush(bgColor);
|
||||||
|
|
||||||
|
var fontBold = new PdfSharp.Drawing.XFont("Arial", 7.5, PdfSharp.Drawing.XFontStyleEx.Bold);
|
||||||
|
var fontNormal = new PdfSharp.Drawing.XFont("Arial", 6.5, PdfSharp.Drawing.XFontStyleEx.Regular);
|
||||||
|
var linePen = new PdfSharp.Drawing.XPen(lineColor, 0.5);
|
||||||
|
|
||||||
|
var fmtLeft = new PdfSharp.Drawing.XStringFormat
|
||||||
|
{
|
||||||
|
Alignment = PdfSharp.Drawing.XStringAlignment.Near,
|
||||||
|
LineAlignment = PdfSharp.Drawing.XLineAlignment.Near,
|
||||||
|
};
|
||||||
|
|
||||||
|
var date = DateTime.Now.ToString("dd.MM.yyyy");
|
||||||
|
|
||||||
|
foreach (var field in signatures)
|
||||||
|
{
|
||||||
|
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);
|
||||||
|
|
||||||
|
double x = field.X;
|
||||||
|
double y = field.Y;
|
||||||
|
|
||||||
|
// --- Calculate layout positions first (needed for bg rect) ---
|
||||||
|
double imgH = sigH * imgRatio;
|
||||||
|
double lineY = y + imgH + 1.0; // 1pt gap between image and separator
|
||||||
|
double textY = lineY + 1.5; // 1.5pt gap below separator line
|
||||||
|
double padding = 3;
|
||||||
|
|
||||||
|
// Row 1: FullName
|
||||||
|
double row1Y = textY;
|
||||||
|
// Row 2: Position (optional)
|
||||||
|
double row2Y = row1Y + lineH;
|
||||||
|
// Row 3: Place, Date — immediately after row2 regardless of position
|
||||||
|
double row3Y = !string.IsNullOrWhiteSpace(sig.Position) ? row2Y + lineH : row2Y;
|
||||||
|
double contentBottom = row3Y + lineH;
|
||||||
|
|
||||||
|
// --- Background rectangle sized to actual content (not full sigH) ---
|
||||||
|
var bgRect = new PdfSharp.Drawing.XRect(
|
||||||
|
x - bgPad,
|
||||||
|
y - bgPad,
|
||||||
|
sigW + bgPad * 2,
|
||||||
|
(contentBottom - y) + bgPad * 2);
|
||||||
|
gfx.DrawRectangle(bgBrush, bgRect);
|
||||||
|
|
||||||
|
// --- Image area ---
|
||||||
|
var imgRect = new PdfSharp.Drawing.XRect(x, y, sigW, imgH);
|
||||||
|
using var imgStream = new System.IO.MemoryStream(imgBytes);
|
||||||
|
var xImg = PdfSharp.Drawing.XImage.FromStream(imgStream);
|
||||||
|
gfx.DrawImage(xImg, imgRect);
|
||||||
|
|
||||||
|
// --- Separator line ---
|
||||||
|
gfx.DrawLine(linePen, x + 2, lineY, x + sigW - 2, lineY);
|
||||||
|
|
||||||
|
// --- Text rows ---
|
||||||
|
// Row 1: FullName (bold)
|
||||||
|
var nameRect = new PdfSharp.Drawing.XRect(x + padding, row1Y, sigW - padding * 2, lineH);
|
||||||
|
gfx.DrawString(sig.FullName, fontBold, new PdfSharp.Drawing.XSolidBrush(black), nameRect, fmtLeft);
|
||||||
|
|
||||||
|
// Row 2: Position (optional)
|
||||||
|
if (!string.IsNullOrWhiteSpace(sig.Position))
|
||||||
|
{
|
||||||
|
var posRect = new PdfSharp.Drawing.XRect(x + padding, row2Y, sigW - padding * 2, lineH);
|
||||||
|
gfx.DrawString(sig.Position, fontNormal, new PdfSharp.Drawing.XSolidBrush(darkGray), posRect, fmtLeft);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Row 3: Place, Date
|
||||||
|
var placeDate = $"{sig.Place}, {date}";
|
||||||
|
var dateRect = new PdfSharp.Drawing.XRect(x + padding, row3Y, sigW - padding * 2, lineH);
|
||||||
|
gfx.DrawString(placeDate, fontNormal, new PdfSharp.Drawing.XSolidBrush(darkGray), dateRect, fmtLeft);
|
||||||
|
}
|
||||||
|
|
||||||
|
document.Save(outputMs);
|
||||||
|
return outputMs.ToArray();
|
||||||
|
}
|
||||||
|
|
||||||
|
static byte[]? DataUrlToBytes(string? dataUrl)
|
||||||
|
{
|
||||||
|
if (string.IsNullOrWhiteSpace(dataUrl)) return null;
|
||||||
|
var comma = dataUrl.IndexOf(',');
|
||||||
|
if (comma < 0) return null;
|
||||||
|
return Convert.FromBase64String(dataUrl[(comma + 1)..]);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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
|
||||||
|
|||||||
@@ -3,7 +3,7 @@ using EnvelopeGenerator.Application.Receivers.Queries;
|
|||||||
using Microsoft.AspNetCore.Authorization;
|
using Microsoft.AspNetCore.Authorization;
|
||||||
using Microsoft.AspNetCore.Mvc;
|
using Microsoft.AspNetCore.Mvc;
|
||||||
|
|
||||||
namespace EnvelopeGenerator.GeneratorAPI.Controllers;
|
namespace EnvelopeGenerator.Server.Controllers;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Controller für die Verwaltung von Empfängern.
|
/// Controller für die Verwaltung von Empfängern.
|
||||||
@@ -33,15 +33,16 @@ public class ReceiverController : ControllerBase
|
|||||||
/// <param name="receiver">Die Abfrageparameter, einschließlich E-Mail-Adresse und Signatur.</param>
|
/// <param name="receiver">Die Abfrageparameter, einschließlich E-Mail-Adresse und Signatur.</param>
|
||||||
/// <returns>Eine Liste von Empfängern oder ein Fehlerstatus.</returns>
|
/// <returns>Eine Liste von Empfängern oder ein Fehlerstatus.</returns>
|
||||||
[HttpGet]
|
[HttpGet]
|
||||||
public async Task<IActionResult> Get([FromQuery] ReadReceiverQuery receiver)
|
[Authorize(AuthenticationSchemes = AuthScheme.Sender)]
|
||||||
|
public async Task<IActionResult> Get([FromQuery] ReadReceiverQuery? receiver = null, [FromQuery] bool onlyEmailAddress = false)
|
||||||
{
|
{
|
||||||
if (!receiver.HasAnyCriteria)
|
var result = await _mediator.Send(receiver ?? new ReadReceiverQuery());
|
||||||
{
|
|
||||||
var all = await _mediator.Send(new ReadReceiverQuery());
|
|
||||||
return Ok(all);
|
|
||||||
}
|
|
||||||
|
|
||||||
var result = await _mediator.Send(receiver);
|
if (result is null)
|
||||||
return result is null ? NotFound() : Ok(result);
|
return NotFound();
|
||||||
|
else if (onlyEmailAddress)
|
||||||
|
return Ok(result.Select(r => r.EmailAddress).ToList());
|
||||||
|
else
|
||||||
|
return Ok(result);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -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>
|
||||||
@@ -28,13 +39,12 @@
|
|||||||
<PackageReference Include="DigitalData.Core.API" Version="2.2.1" />
|
<PackageReference Include="DigitalData.Core.API" Version="2.2.1" />
|
||||||
<PackageReference Include="HtmlSanitizer" Version="9.0.892" />
|
<PackageReference Include="HtmlSanitizer" Version="9.0.892" />
|
||||||
<PackageReference Include="Microsoft.Extensions.Caching.SqlServer" Version="8.0.11" />
|
<PackageReference Include="Microsoft.Extensions.Caching.SqlServer" Version="8.0.11" />
|
||||||
<PackageReference Include="itext" Version="8.0.5" />
|
|
||||||
<PackageReference Include="itext.bouncy-castle-adapter" Version="8.0.5" />
|
|
||||||
<PackageReference Include="Microsoft.AspNetCore.OpenApi" Version="8.0.11" />
|
<PackageReference Include="Microsoft.AspNetCore.OpenApi" Version="8.0.11" />
|
||||||
<PackageReference Include="Microsoft.EntityFrameworkCore.SqlServer" Version="8.0.17" />
|
<PackageReference Include="Microsoft.EntityFrameworkCore.SqlServer" Version="8.0.17" />
|
||||||
<PackageReference Include="Microsoft.Identity.Client" Version="4.82.1" />
|
<PackageReference Include="Microsoft.Identity.Client" Version="4.82.1" />
|
||||||
<PackageReference Include="NLog" Version="5.2.5" />
|
<PackageReference Include="NLog" Version="5.2.5" />
|
||||||
<PackageReference Include="NLog.Web.AspNetCore" Version="5.3.0" />
|
<PackageReference Include="NLog.Web.AspNetCore" Version="5.3.0" />
|
||||||
|
<PackageReference Include="PDFsharp" Version="6.2.4" />
|
||||||
<PackageReference Include="Scalar.AspNetCore" Version="2.2.1" />
|
<PackageReference Include="Scalar.AspNetCore" Version="2.2.1" />
|
||||||
<PackageReference Include="SixLabors.ImageSharp" Version="3.1.12" />
|
<PackageReference Include="SixLabors.ImageSharp" Version="3.1.12" />
|
||||||
<PackageReference Include="Swashbuckle.AspNetCore" Version="8.1.1" />
|
<PackageReference Include="Swashbuckle.AspNetCore" Version="8.1.1" />
|
||||||
|
|||||||
@@ -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[]>() ??
|
||||||
@@ -104,7 +108,7 @@ try
|
|||||||
{
|
{
|
||||||
Version = "v1",
|
Version = "v1",
|
||||||
Title = "signFLOW Absender-API",
|
Title = "signFLOW Absender-API",
|
||||||
Description = "Eine API zur Verwaltung der Erstellung, des Versands und der Nachverfolgung von Umschlägen in der signFLOW-Anwendung.",
|
Description = "Eine API zur Verwaltung der Erstellung, des Versands und der Nachverfolgung von Umschl<EFBFBD>gen in der signFLOW-Anwendung.",
|
||||||
Contact = new OpenApiContact
|
Contact = new OpenApiContact
|
||||||
{
|
{
|
||||||
Name = "Digital Data GmbH",
|
Name = "Digital Data GmbH",
|
||||||
@@ -338,6 +342,10 @@ try
|
|||||||
builder.Services.AddDevExpressBlazor();
|
builder.Services.AddDevExpressBlazor();
|
||||||
builder.Services.AddDevExpressServerSideBlazorPdfViewer();
|
builder.Services.AddDevExpressServerSideBlazorPdfViewer();
|
||||||
|
|
||||||
|
// PdfSharp font resolver — required for .NET 8 (no system font access without it)
|
||||||
|
PdfSharp.Fonts.GlobalFontSettings.FontResolver =
|
||||||
|
EnvelopeGenerator.Server.Services.PdfSharpFontResolver.Instance;
|
||||||
|
|
||||||
// Configuration Options
|
// Configuration Options
|
||||||
builder.Services.Configure<EnvelopeGenerator.Server.Client.Options.ApiOptions>(
|
builder.Services.Configure<EnvelopeGenerator.Server.Client.Options.ApiOptions>(
|
||||||
builder.Configuration.GetSection("ApiOptions"));
|
builder.Configuration.GetSection("ApiOptions"));
|
||||||
|
|||||||
@@ -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 |
|
||||||
@@ -4,12 +4,14 @@ using EnvelopeGenerator.Application.Common.Dto;
|
|||||||
using EnvelopeGenerator.Application.Common.Dto.EnvelopeReceiver;
|
using EnvelopeGenerator.Application.Common.Dto.EnvelopeReceiver;
|
||||||
using EnvelopeGenerator.Application.Documents.Queries;
|
using EnvelopeGenerator.Application.Documents.Queries;
|
||||||
using EnvelopeGenerator.Application.EnvelopeReceivers.Queries;
|
using EnvelopeGenerator.Application.EnvelopeReceivers.Queries;
|
||||||
|
using EnvelopeGenerator.Application.Receivers.Queries;
|
||||||
using EnvelopeGenerator.Server.Client.Models;
|
using EnvelopeGenerator.Server.Client.Models;
|
||||||
using EnvelopeGenerator.Server.Client.Models.Constants;
|
using EnvelopeGenerator.Server.Client.Models.Constants;
|
||||||
using EnvelopeGenerator.Server.Extensions;
|
using EnvelopeGenerator.Server.Extensions;
|
||||||
using EnvelopeGenerator.Server.Options;
|
using EnvelopeGenerator.Server.Options;
|
||||||
using MediatR;
|
using MediatR;
|
||||||
using Microsoft.Extensions.Caching.Distributed;
|
using Microsoft.Extensions.Caching.Distributed;
|
||||||
|
using Microsoft.Extensions.Caching.Memory;
|
||||||
using Microsoft.Extensions.Options;
|
using Microsoft.Extensions.Options;
|
||||||
using ApplicationEnvelopeReceiverDto = EnvelopeGenerator.Application.Common.Dto.EnvelopeReceiver.EnvelopeReceiverDto;
|
using ApplicationEnvelopeReceiverDto = EnvelopeGenerator.Application.Common.Dto.EnvelopeReceiver.EnvelopeReceiverDto;
|
||||||
|
|
||||||
@@ -21,7 +23,8 @@ namespace EnvelopeGenerator.Server.Services;
|
|||||||
public class EnvelopeReceiverPageDataService(
|
public class EnvelopeReceiverPageDataService(
|
||||||
IMediator mediator,
|
IMediator mediator,
|
||||||
IDistributedCache cache,
|
IDistributedCache cache,
|
||||||
IOptions<CacheOptions> cacheOptions)
|
IOptions<CacheOptions> cacheOptions,
|
||||||
|
IMemoryCache memoryCache)
|
||||||
{
|
{
|
||||||
private const string SignatureCacheKeyPrefix = "envelope-generator.receiver-ui.signature:";
|
private const string SignatureCacheKeyPrefix = "envelope-generator.receiver-ui.signature:";
|
||||||
|
|
||||||
@@ -101,4 +104,23 @@ public class EnvelopeReceiverPageDataService(
|
|||||||
Page = element.Page,
|
Page = element.Page,
|
||||||
SenderAppType = (EnvelopeGenerator.Server.Client.Models.Constants.SenderAppType)element.SenderAppType
|
SenderAppType = (EnvelopeGenerator.Server.Client.Models.Constants.SenderAppType)element.SenderAppType
|
||||||
};
|
};
|
||||||
|
|
||||||
|
private static readonly string ReceiverEmailSearchCacheKey = Guid.NewGuid().ToString();
|
||||||
|
|
||||||
|
public async Task<IEnumerable<string>> SearchReceiverEMailsAsync(string emailSearchTerm, CancellationToken cancellationToken = default)
|
||||||
|
{
|
||||||
|
|
||||||
|
return await memoryCache.GetOrCreateAsync(ReceiverEmailSearchCacheKey + emailSearchTerm, async entry =>
|
||||||
|
{
|
||||||
|
var query = new ReadReceiverQuery { EmailAddressSearch = emailSearchTerm };
|
||||||
|
var receivers = await mediator.Send(query, cancellationToken);
|
||||||
|
|
||||||
|
if(receivers.Any())
|
||||||
|
entry.AbsoluteExpirationRelativeToNow = TimeSpan.FromSeconds(30);
|
||||||
|
else
|
||||||
|
entry.AbsoluteExpirationRelativeToNow = TimeSpan.FromSeconds(10);
|
||||||
|
|
||||||
|
return receivers.Select(r => r.EmailAddress);
|
||||||
|
}) ?? [];
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,46 @@
|
|||||||
|
using PdfSharp.Fonts;
|
||||||
|
|
||||||
|
namespace EnvelopeGenerator.Server.Services;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// PdfSharp 6.x IFontResolver for .NET 8.
|
||||||
|
/// PdfSharp cannot access system fonts on .NET Core/8 without an explicit resolver.
|
||||||
|
/// This implementation reads fonts directly from the Windows Fonts folder.
|
||||||
|
/// Register once at startup: GlobalFontSettings.FontResolver = PdfSharpFontResolver.Instance;
|
||||||
|
/// </summary>
|
||||||
|
public class PdfSharpFontResolver : IFontResolver
|
||||||
|
{
|
||||||
|
public static readonly PdfSharpFontResolver Instance = new();
|
||||||
|
|
||||||
|
private static readonly string FontsFolder =
|
||||||
|
Environment.GetFolderPath(Environment.SpecialFolder.Fonts);
|
||||||
|
|
||||||
|
public FontResolverInfo? ResolveTypeface(string familyName, bool isBold, bool isItalic)
|
||||||
|
{
|
||||||
|
var key = familyName.ToLowerInvariant() switch
|
||||||
|
{
|
||||||
|
"arial" => isBold ? "arialbd" : "arial",
|
||||||
|
_ => null
|
||||||
|
};
|
||||||
|
|
||||||
|
return key is null ? null : new FontResolverInfo(key);
|
||||||
|
}
|
||||||
|
|
||||||
|
public byte[] GetFont(string faceName)
|
||||||
|
{
|
||||||
|
var fileName = faceName switch
|
||||||
|
{
|
||||||
|
"arialbd" => "arialbd.ttf",
|
||||||
|
_ => "arial.ttf",
|
||||||
|
};
|
||||||
|
|
||||||
|
var path = Path.Combine(FontsFolder, fileName);
|
||||||
|
|
||||||
|
if (!File.Exists(path))
|
||||||
|
throw new FileNotFoundException(
|
||||||
|
$"Font file not found: {path}. " +
|
||||||
|
"Ensure Arial is installed on the server.");
|
||||||
|
|
||||||
|
return File.ReadAllBytes(path);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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);
|
||||||
|
|||||||
@@ -51,6 +51,41 @@
|
|||||||
overflow: auto;
|
overflow: auto;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.pdf-editor-wrapper {
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sender-editor-pdf-viewer {
|
||||||
|
width: 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 {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sender-editor-pdf-viewer .dxbrv-report-preview-content-flex-item {
|
||||||
|
width: 100%;
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sender-editor-pdf-viewer .dxbrv-report-preview-content {
|
||||||
|
margin-left: auto;
|
||||||
|
margin-right: auto;
|
||||||
|
}
|
||||||
|
|
||||||
.pdf-viewer-container {
|
.pdf-viewer-container {
|
||||||
height: 100%;
|
height: 100%;
|
||||||
display: flex;
|
display: flex;
|
||||||
@@ -546,6 +581,285 @@ body.resizing {
|
|||||||
white-space: nowrap;
|
white-space: nowrap;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.sender-toolbar-action-btn {
|
||||||
|
min-width: auto;
|
||||||
|
padding: 0.5rem 0.75rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sender-toolbar-action-btn--compact {
|
||||||
|
padding: 0.45rem 0.7rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sender-receivers-panel {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 0.625rem;
|
||||||
|
padding: 0.75rem 0.9rem;
|
||||||
|
border-radius: 12px;
|
||||||
|
background: linear-gradient(135deg, rgba(126, 34, 206, 0.05) 0%, rgba(42, 82, 152, 0.05) 100%);
|
||||||
|
border: 1px solid rgba(126, 34, 206, 0.12);
|
||||||
|
}
|
||||||
|
|
||||||
|
.sender-receivers-panel__header {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
gap: 0.75rem;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sender-receivers-panel__title-wrap {
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 0.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sender-receivers-panel__title {
|
||||||
|
font-size: 0.8rem;
|
||||||
|
font-weight: 700;
|
||||||
|
color: #4c1d95;
|
||||||
|
letter-spacing: 0.02em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sender-receivers-panel__count {
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
min-width: 1.5rem;
|
||||||
|
min-height: 1.5rem;
|
||||||
|
padding: 0 0.45rem;
|
||||||
|
border-radius: 999px;
|
||||||
|
background: rgba(79, 70, 229, 0.12);
|
||||||
|
color: #5b21b6;
|
||||||
|
font-size: 0.72rem;
|
||||||
|
font-weight: 700;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sender-receivers-panel__add-btn .dxbl-btn {
|
||||||
|
border-radius: 8px;
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
|
||||||
|
.pdf-toolbar-like-btn .dxbl-btn {
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
gap: 0.4rem;
|
||||||
|
min-height: 34px;
|
||||||
|
padding: 0.45rem 0.85rem;
|
||||||
|
border-radius: 8px;
|
||||||
|
border: 1px solid rgba(126, 34, 206, 0.2);
|
||||||
|
background: linear-gradient(135deg, rgba(126, 34, 206, 0.05) 0%, rgba(42, 82, 152, 0.05) 100%);
|
||||||
|
color: #1e293b;
|
||||||
|
font-size: 0.75rem;
|
||||||
|
font-weight: 600;
|
||||||
|
box-shadow: none;
|
||||||
|
transition: all 0.2s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.pdf-toolbar-like-btn .dxbl-btn:hover:not(:disabled) {
|
||||||
|
background: linear-gradient(135deg, rgba(126, 34, 206, 0.1) 0%, rgba(42, 82, 152, 0.1) 100%);
|
||||||
|
border-color: rgba(126, 34, 206, 0.4);
|
||||||
|
color: #1e293b;
|
||||||
|
transform: translateY(-1px);
|
||||||
|
box-shadow: 0 4px 12px rgba(126, 34, 206, 0.2);
|
||||||
|
}
|
||||||
|
|
||||||
|
.pdf-toolbar-like-btn .dxbl-btn:active:not(:disabled) {
|
||||||
|
transform: translateY(0);
|
||||||
|
box-shadow: 0 2px 6px rgba(126, 34, 206, 0.15);
|
||||||
|
}
|
||||||
|
|
||||||
|
.pdf-toolbar-like-btn--add .dxbl-btn::before,
|
||||||
|
.pdf-toolbar-like-btn--signature .dxbl-btn::before {
|
||||||
|
display: inline-block;
|
||||||
|
font-size: 0.9rem;
|
||||||
|
line-height: 1;
|
||||||
|
font-weight: 700;
|
||||||
|
}
|
||||||
|
|
||||||
|
.pdf-toolbar-like-btn--add .dxbl-btn::before {
|
||||||
|
content: '+';
|
||||||
|
color: #7e22ce;
|
||||||
|
}
|
||||||
|
|
||||||
|
.pdf-toolbar-like-btn--signature .dxbl-btn {
|
||||||
|
color: #7e22ce;
|
||||||
|
}
|
||||||
|
|
||||||
|
.pdf-toolbar-like-btn--signature .dxbl-btn::before {
|
||||||
|
content: '?';
|
||||||
|
color: #7e22ce;
|
||||||
|
}
|
||||||
|
|
||||||
|
.pdf-toolbar-like-btn--signature .dxbl-btn:hover:not(:disabled) {
|
||||||
|
color: #7e22ce;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sender-receivers-panel__empty {
|
||||||
|
font-size: 0.78rem;
|
||||||
|
color: #64748b;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sender-receivers-list {
|
||||||
|
display: flex;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
gap: 0.625rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sender-receiver-chip {
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 0.75rem;
|
||||||
|
min-width: 220px;
|
||||||
|
max-width: 100%;
|
||||||
|
padding: 0.625rem 0.75rem;
|
||||||
|
border-radius: 12px;
|
||||||
|
background: rgba(255, 255, 255, 0.88);
|
||||||
|
border: 1px solid rgba(126, 34, 206, 0.12);
|
||||||
|
box-shadow: 0 6px 16px rgba(15, 23, 42, 0.06);
|
||||||
|
}
|
||||||
|
|
||||||
|
.sender-receiver-chip__body {
|
||||||
|
min-width: 0;
|
||||||
|
flex: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sender-receiver-chip__name {
|
||||||
|
font-size: 0.78rem;
|
||||||
|
font-weight: 700;
|
||||||
|
color: #1f2937;
|
||||||
|
white-space: nowrap;
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sender-receiver-chip__email {
|
||||||
|
margin-top: 0.15rem;
|
||||||
|
font-size: 0.72rem;
|
||||||
|
color: #64748b;
|
||||||
|
white-space: nowrap;
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sender-receiver-chip__phone {
|
||||||
|
margin-top: 0.15rem;
|
||||||
|
font-size: 0.72rem;
|
||||||
|
color: #64748b;
|
||||||
|
white-space: nowrap;
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sender-receiver-popup .dxbl-modal {
|
||||||
|
border-radius: 18px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sender-receiver-popup .dxbl-popup {
|
||||||
|
max-width: min(720px, calc(100vw - 2rem));
|
||||||
|
}
|
||||||
|
|
||||||
|
.sender-receiver-popup .dxbl-popup-content {
|
||||||
|
padding: 1rem 1.25rem 1.1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sender-receiver-popup .dxbl-popup-header {
|
||||||
|
padding: 0.95rem 1.25rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sender-receiver-popup .dxbl-popup-footer {
|
||||||
|
padding: 0.75rem 1.25rem 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sender-receiver-popup__body {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 0.9rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sender-receiver-popup__form-grid {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: minmax(0, 1fr) minmax(0, 1fr);
|
||||||
|
gap: 1rem 1.25rem;
|
||||||
|
align-items: start;
|
||||||
|
min-height: 250px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sender-receiver-popup__field {
|
||||||
|
min-width: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sender-receiver-popup__label {
|
||||||
|
margin-bottom: 0.45rem;
|
||||||
|
font-size: 0.82rem;
|
||||||
|
font-weight: 600;
|
||||||
|
color: #475569;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sender-receiver-popup__field .dxbl-text-edit,
|
||||||
|
.sender-receiver-popup__field .dxbl-dropdown-edit {
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sender-receiver-popup__field .dxbl-input-editor,
|
||||||
|
.sender-receiver-popup__field .dxbl-text-edit-input {
|
||||||
|
min-height: 38px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sender-receiver-popup__suggestions-shell {
|
||||||
|
min-height: 188px;
|
||||||
|
margin-top: 0.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sender-receiver-popup__suggestions {
|
||||||
|
border: 1px solid rgba(126, 34, 206, 0.12);
|
||||||
|
border-radius: 10px;
|
||||||
|
background: rgba(255, 255, 255, 0.98);
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sender-receiver-popup__suggestions .dxbl-listbox {
|
||||||
|
border: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sender-receiver-popup__suggestions .dxbl-listbox-scroll-viewer {
|
||||||
|
max-height: 180px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sender-receiver-popup__hint {
|
||||||
|
font-size: 0.78rem;
|
||||||
|
color: #64748b;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sender-receiver-popup__loading {
|
||||||
|
font-size: 0.78rem;
|
||||||
|
color: #4f46e5;
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sender-receiver-popup__validation {
|
||||||
|
padding: 0.625rem 0.75rem;
|
||||||
|
border-radius: 10px;
|
||||||
|
background: rgba(239, 68, 68, 0.08);
|
||||||
|
color: #b91c1c;
|
||||||
|
font-size: 0.8rem;
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sender-receiver-popup__footer {
|
||||||
|
display: flex;
|
||||||
|
justify-content: flex-end;
|
||||||
|
gap: 0.5rem;
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sender-receiver-popup__footer .dxbl-btn {
|
||||||
|
min-width: 148px;
|
||||||
|
border-radius: 8px;
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
|
||||||
.pdf-frame {
|
.pdf-frame {
|
||||||
background: white;
|
background: white;
|
||||||
border-radius: 16px;
|
border-radius: 16px;
|
||||||
@@ -748,6 +1062,30 @@ body.resizing {
|
|||||||
flex-wrap: wrap;
|
flex-wrap: wrap;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.sender-receivers-panel {
|
||||||
|
padding: 0.625rem 0.75rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sender-receiver-chip {
|
||||||
|
width: 100%;
|
||||||
|
min-width: 0;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sender-receiver-chip__action {
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sender-receiver-chip__action {
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sender-receiver-popup__form-grid {
|
||||||
|
grid-template-columns: 1fr;
|
||||||
|
gap: 0.85rem;
|
||||||
|
min-height: 0;
|
||||||
|
}
|
||||||
|
|
||||||
.envelope-title {
|
.envelope-title {
|
||||||
font-size: 1rem;
|
font-size: 1rem;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,95 @@
|
|||||||
|
window.envelopeEditor = {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the click position normalised to [0,1] relative to the rendered PDF page
|
||||||
|
* element inside DxPdfViewer (or DxReportViewer as fallback).
|
||||||
|
*
|
||||||
|
* Normalising means the result is independent of zoom level: no matter how much the
|
||||||
|
* 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)
|
||||||
|
*/
|
||||||
|
getClickCoordsOnPdfPage: function (viewerCssClass, clientX, clientY) {
|
||||||
|
|
||||||
|
// 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;
|
||||||
|
|
||||||
|
return {
|
||||||
|
normX: normX,
|
||||||
|
normY: normY,
|
||||||
|
pageIndex: targetIndex
|
||||||
|
};
|
||||||
|
}
|
||||||
|
};
|
||||||
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