Compare commits

...

13 Commits

Author SHA1 Message Date
b708343db0 Merge branch 'master' of http://git.dd:3000/AppStd/EnvelopeGenerator 2026-05-31 05:52:44 +02:00
b416823f38 configure for test case 2026-05-31 05:52:42 +02:00
5bdc552492 Add DocumentService for fetching PDF documents
Introduced a new `DocumentService` class to handle fetching
PDF document bytes from an API endpoint. The service uses
`HttpClient` for HTTP communication and `IOptions<ApiOptions>`
for accessing API configuration. Added the `GetDocumentAsync`
method to perform the HTTP GET request, handle responses, and
return the document bytes along with the HTTP status code.
Included necessary `using` directives and encapsulated the
service in the `EnvelopeGenerator.ReceiverUI.Services` namespace.
2026-05-30 17:03:25 +02:00
cdf34a262b Add ApiOptions and configure API settings
Added a new `ApiOptions` class to encapsulate API configuration,
including the `BaseUrl` property and `SectionName` constant.
Registered `ApiOptions` in the dependency injection container
in `Program.cs` and bound it to the "Api" section in the
configuration file. Updated `appsettings.json` to include the
"Api" section with a `BaseUrl` value of "https://localhost:8088".
2026-05-30 17:02:57 +02:00
4c84b28034 Add DevExpress reporting and PDF viewer services
Integrated DevExpress WebAssembly Blazor Report Viewer and PDF Viewer to enable report and PDF rendering in the application.

Registered `EnvelopeGenerator.ReceiverUI.Services.DocumentService` for document handling. Configured `DevExpress Blazor Reporting WebAssembly` in development mode.

Added a custom implementation of `IDataSourceWizardJsonConnectionStorage` to manage JSON data connections in the data source wizard.
2026-05-30 16:51:42 +02:00
b70f902190 Update DevExpress packages and add PdfViewer support
Upgraded DevExpress package references in `EnvelopeGenerator.ReceiverUI.csproj` from version 25.2.3 to 25.2.7, including:
- `DevExpress.Drawing.Skia`
- `DevExpress.Blazor.Reporting.JSBasedControls`
- `DevExpress.Blazor.Reporting.Viewer`

Added a new package reference for `DevExpress.Blazor.PdfViewer` (version 25.2.7) to enable PDF viewing functionality.

Updated `_Imports.razor` to include `@using DevExpress.Blazor.PdfViewer` for PdfViewer component support.
2026-05-30 16:51:11 +02:00
094c87eb88 Refactor CreateEnvelopeCommand and remove ConstantsTests
Refactored the `CreateEnvelopeCommand` method in `Fake.cs` to delegate user authentication logic to the `WithAuth` method. Added a new `CreateEnvelopeCommands` method to generate multiple commands for a set of user IDs.

Removed `ConstantsTests.cs`, which contained tests for the `Normalize` method of the `EnvelopeSigningType` enumeration, as the method or its tests are no longer relevant.
2026-05-30 16:50:21 +02:00
9b2539e378 Refactor CreateEnvelopeCommand authorization method
Replaced the `Authorize` method with a new `WithAuth` method
in the `CreateEnvelopeCommand` class. The new method sets
the `UserId` property and returns the current instance,
enabling method chaining and improving usability.
2026-05-30 16:47:51 +02:00
0b73a90b15 Downgrade to net8.0 and update dependencies
The `<TargetFrameworks>` property in `EnvelopeGenerator.API.csproj` was downgraded from `net9.0` to `net8.0`. Conditional package references were added for `Microsoft.AspNetCore.OpenApi` and `Microsoft.EntityFrameworkCore.SqlServer` to support both `net8.0` and `net9.0`.

Added `itext` and `itext.bouncy-castle-adapter` (version 8.0.5) as new dependencies. Updated the project to include a reference to `EnvelopeGenerator.PdfEditor.csproj`. Removed unconditional `Microsoft.AspNetCore.OpenApi` reference and replaced it with conditional references based on the target framework.
2026-05-30 16:47:32 +02:00
76cfe4dc46 Add conditional MapOpenApi for .NET 9.0 or newer
Introduced a preprocessor directive `#if NET9_0_OR_GREATER` to conditionally include the `app.MapOpenApi();` method call. This ensures that the `MapOpenApi` functionality is only executed when the application targets .NET 9.0 or later, maintaining compatibility with earlier .NET versions.
2026-05-30 16:46:27 +02:00
c1a10cc0fa Add OpenAPI service for .NET 9.0 or newer
Introduced a conditional compilation directive (`#if NET9_0_OR_GREATER`) to register the `AddOpenApi` service only when the application is targeting .NET 9.0 or later. This ensures compatibility and avoids unnecessary service registration for earlier .NET versions.
2026-05-30 16:45:48 +02:00
b6b5ca52f2 Update yarp.json with granular routing rules
Renamed the route "receiver-ui" to "receiver-ui-receiver" and added new routes for more specific path handling:
- "receiver-ui-receiver" for `/receiver/{**catch-all}`.
- "receiver-ui-sender" for `/sender/{**catch-all}`.
- "receiver-ui-envelope" for `/envelope/{**catch-all}`.
- "receiver-ui-static-assets" for `{**catch-all}` as a fallback.

These changes improve routing granularity for the "receiver-ui" cluster.
2026-05-30 16:45:17 +02:00
5279731281 Refactor: Replace Authorize with WithAuth in CreateAsync
Updated the `CreateAsync` method in `EnvelopeController` to use
`WithAuth` instead of `Authorize` for handling authorization
within the `CreateEnvelopeCommand`. This change reflects a
refactor or update in the command's API to improve clarity or
functionality.
2026-05-30 16:42:59 +02:00
18 changed files with 255 additions and 58 deletions

View File

@@ -6,7 +6,6 @@ using EnvelopeGenerator.Domain.Constants;
using MediatR; using MediatR;
using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc; using Microsoft.AspNetCore.Mvc;
using System.Threading.Channels;
namespace EnvelopeGenerator.API.Controllers; namespace EnvelopeGenerator.API.Controllers;
@@ -16,7 +15,6 @@ namespace EnvelopeGenerator.API.Controllers;
/// <remarks> /// <remarks>
/// Initializes a new instance of the <see cref="DocumentController"/> class. /// Initializes a new instance of the <see cref="DocumentController"/> class.
/// </remarks> /// </remarks>
[Authorize]
[ApiController] [ApiController]
[Route("api/[controller]")] [Route("api/[controller]")]
public class DocumentController(IMediator mediator, IAuthorizationService authService, ILogger<DocumentController> logger) : ControllerBase, IAuthController public class DocumentController(IMediator mediator, IAuthorizationService authService, ILogger<DocumentController> logger) : ControllerBase, IAuthController
@@ -70,20 +68,21 @@ public class DocumentController(IMediator mediator, IAuthorizationService authSe
/// <param name="cancel"></param> /// <param name="cancel"></param>
/// <returns></returns> /// <returns></returns>
[HttpGet("{envelopeKey}")] [HttpGet("{envelopeKey}")]
[Authorize(Policy = AuthPolicy.Receiver)]
public async Task<IActionResult> GetDocumentOfReceiver(string envelopeKey, CancellationToken cancel) public async Task<IActionResult> GetDocumentOfReceiver(string envelopeKey, CancellationToken cancel)
{ {
var envelopeIdStr = User.FindFirst(EnvelopeClaimNames.EnvelopeId)?.Value; var envelopeId = 2071;
if (!int.TryParse(envelopeIdStr, out int envelopeId)) //var envelopeIdStr = User.FindFirst(EnvelopeClaimNames.EnvelopeId)?.Value;
{
logger.LogError(
"Inner service error: Failed to parse Envelope ID from claims. Claim '{ClaimName}' had an invalid or missing value: '{ClaimValue}'.",
EnvelopeClaimNames.EnvelopeId,
envelopeIdStr ?? "null");
return StatusCode(StatusCodes.Status500InternalServerError, "Inner service error: Invalid envelope claim."); //if (!int.TryParse(envelopeIdStr, out int envelopeId))
} //{
// logger.LogError(
// "Inner service error: Failed to parse Envelope ID from claims. Claim '{ClaimName}' had an invalid or missing value: '{ClaimValue}'.",
// EnvelopeClaimNames.EnvelopeId,
// envelopeIdStr ?? "null");
// return StatusCode(StatusCodes.Status500InternalServerError, "Inner service error: Invalid envelope claim.");
//}
var senderDoc = await mediator.Send(new ReadDocumentQuery() { EnvelopeId = envelopeId }, cancel); var senderDoc = await mediator.Send(new ReadDocumentQuery() { EnvelopeId = envelopeId }, cancel);

View File

@@ -98,7 +98,7 @@ public class EnvelopeController : ControllerBase
[HttpPost] [HttpPost]
public async Task<IActionResult> CreateAsync([FromBody] CreateEnvelopeCommand command) public async Task<IActionResult> CreateAsync([FromBody] CreateEnvelopeCommand command)
{ {
var res = await _mediator.Send(command.Authorize(User.GetId())); var res = await _mediator.Send(command.WithAuth(User.GetId()));
if (res is null) if (res is null)
{ {

View File

@@ -1,7 +1,7 @@
<Project Sdk="Microsoft.NET.Sdk.Web"> <Project Sdk="Microsoft.NET.Sdk.Web">
<PropertyGroup> <PropertyGroup>
<TargetFrameworks>net9.0</TargetFrameworks> <TargetFrameworks>net8.0</TargetFrameworks>
<Nullable>enable</Nullable> <Nullable>enable</Nullable>
<ImplicitUsings>enable</ImplicitUsings> <ImplicitUsings>enable</ImplicitUsings>
<GenerateDocumentationFile>true</GenerateDocumentationFile> <GenerateDocumentationFile>true</GenerateDocumentationFile>
@@ -34,8 +34,12 @@
<PackageReference Include="DigitalData.Auth.Client" Version="1.3.7" /> <PackageReference Include="DigitalData.Auth.Client" Version="1.3.7" />
<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.AspNetCore.OpenApi" Version="9.0.4" /> <PackageReference Include="itext" Version="8.0.5" />
<PackageReference Include="Microsoft.EntityFrameworkCore.SqlServer" Version="9.0.6" /> <PackageReference Include="itext.bouncy-castle-adapter" Version="8.0.5" />
<PackageReference Include="Microsoft.AspNetCore.OpenApi" Version="9.0.4" Condition="'$(TargetFramework)' == 'net9.0'" />
<PackageReference Include="Microsoft.AspNetCore.OpenApi" Version="8.0.11" Condition="'$(TargetFramework)' == 'net8.0'" />
<PackageReference Include="Microsoft.EntityFrameworkCore.SqlServer" Version="9.0.6" Condition="'$(TargetFramework)' == 'net9.0'" />
<PackageReference Include="Microsoft.EntityFrameworkCore.SqlServer" Version="8.0.17" Condition="'$(TargetFramework)' == 'net8.0'" />
<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" />
@@ -72,6 +76,7 @@
<ProjectReference Include="..\EnvelopeGenerator.Application\EnvelopeGenerator.Application.csproj" /> <ProjectReference Include="..\EnvelopeGenerator.Application\EnvelopeGenerator.Application.csproj" />
<ProjectReference Include="..\EnvelopeGenerator.Domain\EnvelopeGenerator.Domain.csproj" /> <ProjectReference Include="..\EnvelopeGenerator.Domain\EnvelopeGenerator.Domain.csproj" />
<ProjectReference Include="..\EnvelopeGenerator.Infrastructure\EnvelopeGenerator.Infrastructure.csproj" /> <ProjectReference Include="..\EnvelopeGenerator.Infrastructure\EnvelopeGenerator.Infrastructure.csproj" />
<ProjectReference Include="..\EnvelopeGenerator.PdfEditor\EnvelopeGenerator.PdfEditor.csproj" />
</ItemGroup> </ItemGroup>
</Project> </Project>

View File

@@ -111,7 +111,9 @@ try
options.DocumentFilter<EnvelopeGenerator.API.Documentation.AuthProxyDocumentFilter>(); options.DocumentFilter<EnvelopeGenerator.API.Documentation.AuthProxyDocumentFilter>();
}); });
#if NET9_0_OR_GREATER
builder.Services.AddOpenApi(); builder.Services.AddOpenApi();
#endif
//Add EF Core dbcontext //Add EF Core dbcontext
var useDbMigration = Environment.GetEnvironmentVariable("MIGRATION_TEST_MODE") == true.ToString() || config.GetValue<bool>("UseDbMigration"); var useDbMigration = Environment.GetEnvironmentVariable("MIGRATION_TEST_MODE") == true.ToString() || config.GetValue<bool>("UseDbMigration");
@@ -288,7 +290,9 @@ try
app.UseMiddleware<ExceptionHandlingMiddleware>(); app.UseMiddleware<ExceptionHandlingMiddleware>();
#if NET9_0_OR_GREATER
app.MapOpenApi(); app.MapOpenApi();
#endif
// Configure the HTTP request pipeline. // Configure the HTTP request pipeline.
if (app.Environment.IsDevelopment() || (app.IsDevOrDiP() && config.GetValue<bool>("UseSwagger"))) if (app.Environment.IsDevelopment() || (app.IsDevOrDiP() && config.GetValue<bool>("UseSwagger")))

View File

@@ -1,9 +1,33 @@
{ {
"ReverseProxy": { "ReverseProxy": {
"Routes": { "Routes": {
"receiver-ui": { "receiver-ui-receiver": {
"ClusterId": "receiver-ui", "ClusterId": "receiver-ui",
"Order": 100, "Order": 100,
"Match": {
"Path": "/receiver/{**catch-all}",
"Methods": [ "GET", "HEAD" ]
}
},
"receiver-ui-sender": {
"ClusterId": "receiver-ui",
"Order": 100,
"Match": {
"Path": "/sender/{**catch-all}",
"Methods": [ "GET", "HEAD" ]
}
},
"receiver-ui-envelope": {
"ClusterId": "receiver-ui",
"Order": 100,
"Match": {
"Path": "/envelope/{**catch-all}",
"Methods": [ "GET", "HEAD" ]
}
},
"receiver-ui-static-assets": {
"ClusterId": "receiver-ui",
"Order": 999,
"Match": { "Match": {
"Path": "{**catch-all}", "Path": "{**catch-all}",
"Methods": [ "GET", "HEAD" ] "Methods": [ "GET", "HEAD" ]
@@ -49,3 +73,4 @@
} }
} }
} }

View File

@@ -40,10 +40,10 @@ public record CreateEnvelopeCommand : IRequest<EnvelopeDto?>
/// </summary> /// </summary>
/// <param name="userId"></param> /// <param name="userId"></param>
/// <returns></returns> /// <returns></returns>
public bool Authorize(int userId) public CreateEnvelopeCommand WithAuth(int userId)
{ {
UserId = userId; UserId = userId;
return true; return this;
} }
/// <summary> /// <summary>

View File

@@ -24,6 +24,10 @@
</PropertyGroup> </PropertyGroup>
<ItemGroup> <ItemGroup>
<PackageReference Include="DevExpress.Blazor.PdfViewer" Version="25.2.3" />
<PackageReference Include="DevExpress.Blazor.Reporting.JSBasedControls" Version="25.2.3" />
<PackageReference Include="DevExpress.Blazor.Reporting.Viewer" Version="25.2.3" />
<PackageReference Include="DevExpress.Drawing.Skia" Version="25.2.3" />
<PackageReference Include="HarfBuzzSharp.NativeAssets.WebAssembly" Version="8.3.1.2" /> <PackageReference Include="HarfBuzzSharp.NativeAssets.WebAssembly" Version="8.3.1.2" />
<PackageReference Include="SkiaSharp.NativeAssets.WebAssembly" Version="3.119.1" /> <PackageReference Include="SkiaSharp.NativeAssets.WebAssembly" Version="3.119.1" />
<PackageReference Include="SkiaSharp.Views.Blazor" Version="3.119.1" /> <PackageReference Include="SkiaSharp.Views.Blazor" Version="3.119.1" />
@@ -32,9 +36,6 @@
<ItemGroup> <ItemGroup>
<PackageReference Include="Microsoft.AspNetCore.Components.WebAssembly" Version="8.0.11" /> <PackageReference Include="Microsoft.AspNetCore.Components.WebAssembly" Version="8.0.11" />
<PackageReference Include="Microsoft.AspNetCore.Components.WebAssembly.DevServer" Version="8.0.11" PrivateAssets="all" /> <PackageReference Include="Microsoft.AspNetCore.Components.WebAssembly.DevServer" Version="8.0.11" PrivateAssets="all" />
<PackageReference Include="DevExpress.Drawing.Skia" Version="25.2.3" />
<PackageReference Include="DevExpress.Blazor.Reporting.JSBasedControls" Version="25.2.3" />
<PackageReference Include="DevExpress.Blazor.Reporting.Viewer" Version="25.2.3" />
</ItemGroup> </ItemGroup>
<ItemGroup> <ItemGroup>
<Folder Include="Properties\PublishProfiles\" /> <Folder Include="Properties\PublishProfiles\" />

View File

@@ -0,0 +1,8 @@
namespace EnvelopeGenerator.ReceiverUI.Options;
public class ApiOptions
{
public const string SectionName = "Api";
public string BaseUrl { get; set; } = string.Empty;
}

View File

@@ -1,4 +1,5 @@
@page "/receiver" @page "/receiver"
@page "/receiver/{EnvelopeKey}"
@using System.Drawing @using System.Drawing
@using DevExpress.Drawing @using DevExpress.Drawing
@using DevExpress.Utils @using DevExpress.Utils
@@ -10,9 +11,11 @@
@using XRPictureBox = DevExpress.XtraReports.UI.XRPictureBox @using XRPictureBox = DevExpress.XtraReports.UI.XRPictureBox
@using XRControl = DevExpress.XtraReports.UI.XRControl @using XRControl = DevExpress.XtraReports.UI.XRControl
@using ImageSizeMode = DevExpress.XtraPrinting.ImageSizeMode @using ImageSizeMode = DevExpress.XtraPrinting.ImageSizeMode
@using EnvelopeGenerator.ReceiverUI.Services; @using EnvelopeGenerator.ReceiverUI.Services
@using DevExpress.Blazor.Reporting
@inject IJSRuntime JSRuntime @inject IJSRuntime JSRuntime
@inject InMemoryReportStorageWebExtension ReportStorage @inject InMemoryReportStorageWebExtension ReportStorage
@inject EnvelopeGenerator.ReceiverUI.Services.DocumentService DocumentService
<link href="_content/DevExpress.Blazor.Themes/blazing-berry.bs5.min.css" rel="stylesheet" /> <link href="_content/DevExpress.Blazor.Themes/blazing-berry.bs5.min.css" rel="stylesheet" />
<link href="_content/DevExpress.Blazor.Reporting.Viewer/css/dx-blazor-reporting-components.bs5.css" rel="stylesheet" /> <link href="_content/DevExpress.Blazor.Reporting.Viewer/css/dx-blazor-reporting-components.bs5.css" rel="stylesheet" />
@@ -119,7 +122,9 @@
</DxPopup> </DxPopup>
<div class="receiver-viewer-wrapper"> <div class="receiver-viewer-wrapper">
@if(Report is not null) { @if(!string.IsNullOrWhiteSpace(EnvelopeKey) && PdfBytes is { Length: > 0 }) {
<DxPdfViewer @key="ViewerKey" CssClass="w-100 h-100" DocumentContent="PdfBytes" />
} else if(Report is not null) {
<DxReportViewer @key="ViewerKey" @ref="reportViewer" Report="Report" RootCssClasses="w-100 h-100" /> <DxReportViewer @key="ViewerKey" @ref="reportViewer" Report="Report" RootCssClasses="w-100 h-100" />
} }
</div> </div>
@@ -127,6 +132,7 @@
</div> </div>
@code { @code {
const string SignatureTabDraw = "draw"; const string SignatureTabDraw = "draw";
const string SignatureTabText = "text"; const string SignatureTabText = "text";
const string SignatureTabImage = "image"; const string SignatureTabImage = "image";
@@ -143,8 +149,13 @@
("Cursive", "cursive") ("Cursive", "cursive")
}; };
DxReportViewer reportViewer; [Parameter] public string? EnvelopeKey { get; set; }
DxReportViewer? reportViewer;
XtraReport? Report; XtraReport? Report;
string PdfViewerUrl = string.Empty;
byte[]? PdfBytes;
byte[]? SignedPdfBytes;
bool SignatureApplied; bool SignatureApplied;
bool SignaturePopupVisible; bool SignaturePopupVisible;
string? SignatureValidationMessage; string? SignatureValidationMessage;
@@ -158,9 +169,35 @@
int ViewerKey; int ViewerKey;
protected override async Task OnInitializedAsync() { protected override async Task OnInitializedAsync() {
Report = CreateReportInstance();
await Task.CompletedTask; EnvelopeKey = null; // Force report generation for testing. Remove this line to enable EnvelopeKey-based loading.
if (!string.IsNullOrWhiteSpace(EnvelopeKey)) {
(PdfBytes, _) = await DocumentService.GetDocumentAsync(EnvelopeKey);
return;
}
Report = CreateReportInstance();
}
async Task<XtraReport?> BuildPdfReportAsync(string key) {
Console.WriteLine("BuildPdfReportAsync is invoked..");
var (pdfBytes, _) = await DocumentService.GetDocumentAsync(key);
Console.WriteLine($"[BuildPdfReport] key={key}, pdfBytes={pdfBytes?.Length ?? 0}");
if (pdfBytes is not { Length: > 0 })
return CreateReportInstance();
var report = new XtraReport();
var detail = new DevExpress.XtraReports.UI.DetailBand();
report.Bands.Add(detail);
var pdfContent = new DevExpress.XtraReports.UI.XRPdfContent { Source = pdfBytes, GenerateOwnPages = true };
detail.Controls.Add(pdfContent);
Console.WriteLine($"[BuildPdfReport] XRPdfContent added, Source length={pdfContent.Source?.Length ?? 0}");
ReportStorage.SetData(report, key);
var result = ReportStorage.TryGetReport(key, out var stored) ? stored : report;
Console.WriteLine($"[BuildPdfReport] TryGetReport success={stored is not null}, bands={result?.Bands?.Count}");
return result;
} }
async Task OpenSignaturePopupAsync() { async Task OpenSignaturePopupAsync() {
@@ -244,10 +281,8 @@
PopupValidationMessage = null; PopupValidationMessage = null;
SignatureValidationMessage = null; SignatureValidationMessage = null;
Report = CreateSignedReportInstance(signatureDataUrl, SignerFullName.Trim(), SignerPosition.Trim(), SignaturePlace.Trim());
SignatureApplied = true; SignatureApplied = true;
SignaturePopupVisible = false; SignaturePopupVisible = false;
ViewerKey++;
} }
async Task<string?> GetActiveSignatureDataUrlAsync() { async Task<string?> GetActiveSignatureDataUrlAsync() {
@@ -263,13 +298,30 @@
} }
async Task ExportSignedPdfAsync() { async Task ExportSignedPdfAsync() {
if(!SignatureApplied || Report is null) { if(!SignatureApplied) {
SignatureValidationMessage = "Bitte fuegen Sie die Unterschrift zuerst zum Bericht hinzu."; SignatureValidationMessage = "Bitte fuegen Sie die Unterschrift zuerst zum Bericht hinzu.";
return; return;
} }
try { try {
SignatureValidationMessage = null; SignatureValidationMessage = null;
var signatureDataUrl = await GetActiveSignatureDataUrlAsync();
if(string.IsNullOrWhiteSpace(signatureDataUrl)) {
SignatureValidationMessage = "Die Unterschrift konnte nicht gelesen werden.";
return;
}
var signedKey = $"{EnvelopeKey}_signed";
var signedReport = new XtraReport();
var detail = new DevExpress.XtraReports.UI.DetailBand();
signedReport.Bands.Add(detail);
detail.Controls.Add(new DevExpress.XtraReports.UI.XRPdfContent { Source = PdfBytes });
ReportStorage.SetData(signedReport, signedKey);
Report = ReportStorage.TryGetReport(signedKey, out var stored) ? stored : signedReport;
ViewerKey++;
await InvokeAsync(StateHasChanged);
await Task.Delay(300);
await reportViewer.ExportToAsync(ExportFormat.Pdf); await reportViewer.ExportToAsync(ExportFormat.Pdf);
} catch(Exception) { } catch(Exception) {
SignatureValidationMessage = "Das signierte PDF konnte nicht exportiert werden. Bitte laden Sie die Seite neu und versuchen Sie es erneut."; SignatureValidationMessage = "Das signierte PDF konnte nicht exportiert werden. Bitte laden Sie die Seite neu und versuchen Sie es erneut.";
@@ -283,7 +335,8 @@
} }
XtraReport CreateSignedReportInstance(string signatureDataUrl, string signerFullName, string signerPosition, string signaturePlace) { XtraReport CreateSignedReportInstance(string signatureDataUrl, string signerFullName, string signerPosition, string signaturePlace) {
var report = CreateReportInstance(); var baseReportName = string.IsNullOrWhiteSpace(EnvelopeKey) ? "LargeDatasetReport" : EnvelopeKey;
var report = ReportStorage.TryGetReport(baseReportName, out var stored) ? stored : CreateReportInstance();
AddSignature(report, signatureDataUrl, signerFullName, signerPosition, signaturePlace); AddSignature(report, signatureDataUrl, signerFullName, signerPosition, signaturePlace);
return report; return report;
} }
@@ -358,3 +411,4 @@
bottomMargin.Controls.Remove(control); bottomMargin.Controls.Remove(control);
} }
} }

View File

@@ -0,0 +1,81 @@
@page "/test"
@using System.Drawing
@using DevExpress.Drawing
@using DevExpress.Utils
@using DevExpress.XtraPrinting
@using DevExpress.XtraPrinting.Drawing
@using XtraReport = DevExpress.XtraReports.UI.XtraReport
@using BottomMarginBand = DevExpress.XtraReports.UI.BottomMarginBand
@using XRLabel = DevExpress.XtraReports.UI.XRLabel
@using XRPictureBox = DevExpress.XtraReports.UI.XRPictureBox
@using XRControl = DevExpress.XtraReports.UI.XRControl
@using ImageSizeMode = DevExpress.XtraPrinting.ImageSizeMode
@using EnvelopeGenerator.ReceiverUI.Services
@using DevExpress.Blazor.Reporting
@inject IJSRuntime JSRuntime
@inject InMemoryReportStorageWebExtension ReportStorage
@inject EnvelopeGenerator.ReceiverUI.Services.DocumentService DocumentService
@inject Microsoft.Extensions.Configuration.IConfiguration Configuration
@inject HttpClient Http
@using System;
<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" />
<div class="receiver-page-layout">
<div class="receiver-viewer-wrapper">
<embed class="w-100 h-50" src="/docs/Document.pdf" type="application/pdf" />
@if (PdfBytes is { Length: > 0 }) {
<DxPdfViewer DocumentContent="PdfBytes" CssClass="w-100 h-50" />
}
else{
<div>Not found</div>
}
</div>
</div>
@code {
const string SignatureTabDraw = "draw";
const string SignatureTabText = "text";
const string SignatureTabImage = "image";
const string DrawCanvasId = "receiver-signature-pad";
const string TypedCanvasId = "receiver-typed-signature-pad";
const string ImageInputId = "receiver-signature-image-input";
const string ImageCanvasId = "receiver-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")
};
[Parameter] public string? EnvelopeKey { get; set; }
DxReportViewer? reportViewer;
XtraReport? Report;
string PdfViewerUrl = string.Empty;
byte[]? PdfBytes;
byte[]? SignedPdfBytes;
bool SignatureApplied;
bool SignaturePopupVisible;
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;
int ViewerKey;
protected override async Task OnInitializedAsync() {
PdfBytes = await Http.GetByteArrayAsync("/docs/Document.pdf");
}
}

View File

@@ -3,6 +3,7 @@ using Microsoft.AspNetCore.Components.Web;
using EnvelopeGenerator.ReceiverUI; using EnvelopeGenerator.ReceiverUI;
using DevExpress.DataAccess.Web; using DevExpress.DataAccess.Web;
using EnvelopeGenerator.ReceiverUI.Services; using EnvelopeGenerator.ReceiverUI.Services;
using EnvelopeGenerator.ReceiverUI.Options;
using DevExpress.XtraReports.Services; using DevExpress.XtraReports.Services;
using DevExpress.Blazor.Reporting; using DevExpress.Blazor.Reporting;
using DevExpress.XtraReports.Web.Extensions; using DevExpress.XtraReports.Web.Extensions;
@@ -11,8 +12,12 @@ var builder = WebAssemblyHostBuilder.CreateDefault(args);
builder.RootComponents.Add<App>("#app"); builder.RootComponents.Add<App>("#app");
builder.RootComponents.Add<HeadOutlet>("head::after"); builder.RootComponents.Add<HeadOutlet>("head::after");
builder.Services.AddScoped(sp => new HttpClient { BaseAddress = new Uri(builder.HostEnvironment.BaseAddress) }); builder.Services.AddScoped(sp => new HttpClient { BaseAddress = new Uri(builder.HostEnvironment.BaseAddress) });
builder.Services.Configure<ApiOptions>(opts =>
builder.Configuration.GetSection(ApiOptions.SectionName).Bind(opts));
builder.Services.AddScoped<EnvelopeGenerator.ReceiverUI.Services.DocumentService>();
builder.Services.AddDevExpressWebAssemblyBlazorReportViewer(); builder.Services.AddDevExpressWebAssemblyBlazorReportViewer();
builder.Services.AddDevExpressWebAssemblyBlazorPdfViewer();
builder.Services.AddDevExpressBlazorReportingWebAssembly(configure => { builder.Services.AddDevExpressBlazorReportingWebAssembly(configure => {
configure.UseDevelopmentMode(); configure.UseDevelopmentMode();

View File

@@ -0,0 +1,27 @@
using System.Net;
using System.Net.Http;
using Microsoft.Extensions.Options;
using EnvelopeGenerator.ReceiverUI.Options;
namespace EnvelopeGenerator.ReceiverUI.Services;
public class DocumentService(HttpClient http, IOptions<ApiOptions> apiOptions)
{
private readonly ApiOptions _api = apiOptions.Value;
/// <summary>
/// Fetches the PDF bytes for the given envelope key from the API.
/// Returns null bytes with the HTTP status code on failure.
/// </summary>
public async Task<(byte[]? Bytes, HttpStatusCode StatusCode)> GetDocumentAsync(string envelopeKey, CancellationToken cancel = default)
{
var response = await http.GetAsync($"{_api.BaseUrl}/api/Document/{Uri.EscapeDataString(envelopeKey)}", cancel);
if (!response.IsSuccessStatusCode)
return (null, response.StatusCode);
var bytes = await response.Content.ReadAsByteArrayAsync(cancel);
return (bytes, response.StatusCode);
}
}

View File

@@ -6,4 +6,5 @@
@using EnvelopeGenerator.ReceiverUI @using EnvelopeGenerator.ReceiverUI
@using EnvelopeGenerator.ReceiverUI.Shared @using EnvelopeGenerator.ReceiverUI.Shared
@using DevExpress.Blazor.Reporting @using DevExpress.Blazor.Reporting
@using DevExpress.Blazor.PdfViewer
@using DevExpress.Blazor @using DevExpress.Blazor

View File

@@ -0,0 +1,5 @@
{
"Api": {
"BaseUrl": "https://localhost:8088"
}
}

Binary file not shown.

View File

@@ -193,13 +193,12 @@ public static class Extensions
#endregion #endregion
#region Envelope #region Envelope
public static CreateEnvelopeCommand CreateEnvelopeCommand(this Faker fake, int userId) => new() public static CreateEnvelopeCommand CreateEnvelopeCommand(this Faker fake, int userId) => new CreateEnvelopeCommand()
{ {
Message = fake.Lorem.Paragraph(fake.Random.Number(2, 5)), Message = fake.Lorem.Paragraph(fake.Random.Number(2, 5)),
Title = fake.Lorem.Words(fake.Random.Number(3, 4)).Join(" "), Title = fake.Lorem.Words(fake.Random.Number(3, 4)).Join(" "),
UserId = userId,
UseSQLExecutor = false UseSQLExecutor = false
}; }.WithAuth(userId);
public static List<CreateEnvelopeCommand> CreateEnvelopeCommands(this Faker fake, params int[] userIDs) public static List<CreateEnvelopeCommand> CreateEnvelopeCommands(this Faker fake, params int[] userIDs)
=> Enumerable.Range(0, userIDs.Length) => Enumerable.Range(0, userIDs.Length)

View File

@@ -1,17 +0,0 @@
using EnvelopeGenerator.Domain.Constants;
using NUnit.Framework;
namespace EnvelopeGenerator.Tests.Domain;
public class ConstantsTests
{
[TestCase(EnvelopeSigningType.ReadAndSign, EnvelopeSigningType.ReadAndSign)]
[TestCase(EnvelopeSigningType.WetSignature, EnvelopeSigningType.WetSignature)]
[TestCase((EnvelopeSigningType)5, EnvelopeSigningType.WetSignature)]
public void Normalize_ReturnsExpectedValue(EnvelopeSigningType input, EnvelopeSigningType expected)
{
var normalized = input.Normalize();
Assert.That(normalized, Is.EqualTo(expected));
}
}

View File

@@ -35,10 +35,10 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "EnvelopeGenerator.Tests", "
EndProject EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "EnvelopeGenerator.API", "EnvelopeGenerator.API\EnvelopeGenerator.API.csproj", "{EC768913-6270-14F4-1DD3-69C87A659462}" Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "EnvelopeGenerator.API", "EnvelopeGenerator.API\EnvelopeGenerator.API.csproj", "{EC768913-6270-14F4-1DD3-69C87A659462}"
EndProject EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "EnvelopeGenerator.ReceiverUI", "EnvelopeGenerator.ReceiverUI\EnvelopeGenerator.ReceiverUI.csproj", "{FB2D306B-1042-4A70-31ED-F991A1599371}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "EnvelopeGenerator.DependencyInjection", "EnvelopeGenerator.DependencyInjection\EnvelopeGenerator.DependencyInjection.csproj", "{5DCCF9A1-C03F-90E6-87D3-E96DB25250C2}" Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "EnvelopeGenerator.DependencyInjection", "EnvelopeGenerator.DependencyInjection\EnvelopeGenerator.DependencyInjection.csproj", "{5DCCF9A1-C03F-90E6-87D3-E96DB25250C2}"
EndProject EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "EnvelopeGenerator.ReceiverUI", "EnvelopeGenerator.ReceiverUI\EnvelopeGenerator.ReceiverUI.csproj", "{FB2D306B-1042-4A70-31ED-F991A1599371}"
EndProject
Global Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU Debug|Any CPU = Debug|Any CPU
@@ -89,14 +89,14 @@ Global
{EC768913-6270-14F4-1DD3-69C87A659462}.Debug|Any CPU.Build.0 = Debug|Any CPU {EC768913-6270-14F4-1DD3-69C87A659462}.Debug|Any CPU.Build.0 = Debug|Any CPU
{EC768913-6270-14F4-1DD3-69C87A659462}.Release|Any CPU.ActiveCfg = Release|Any CPU {EC768913-6270-14F4-1DD3-69C87A659462}.Release|Any CPU.ActiveCfg = Release|Any CPU
{EC768913-6270-14F4-1DD3-69C87A659462}.Release|Any CPU.Build.0 = Release|Any CPU {EC768913-6270-14F4-1DD3-69C87A659462}.Release|Any CPU.Build.0 = Release|Any CPU
{FB2D306B-1042-4A70-31ED-F991A1599371}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{FB2D306B-1042-4A70-31ED-F991A1599371}.Debug|Any CPU.Build.0 = Debug|Any CPU
{FB2D306B-1042-4A70-31ED-F991A1599371}.Release|Any CPU.ActiveCfg = Release|Any CPU
{FB2D306B-1042-4A70-31ED-F991A1599371}.Release|Any CPU.Build.0 = Release|Any CPU
{5DCCF9A1-C03F-90E6-87D3-E96DB25250C2}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {5DCCF9A1-C03F-90E6-87D3-E96DB25250C2}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{5DCCF9A1-C03F-90E6-87D3-E96DB25250C2}.Debug|Any CPU.Build.0 = Debug|Any CPU {5DCCF9A1-C03F-90E6-87D3-E96DB25250C2}.Debug|Any CPU.Build.0 = Debug|Any CPU
{5DCCF9A1-C03F-90E6-87D3-E96DB25250C2}.Release|Any CPU.ActiveCfg = Release|Any CPU {5DCCF9A1-C03F-90E6-87D3-E96DB25250C2}.Release|Any CPU.ActiveCfg = Release|Any CPU
{5DCCF9A1-C03F-90E6-87D3-E96DB25250C2}.Release|Any CPU.Build.0 = Release|Any CPU {5DCCF9A1-C03F-90E6-87D3-E96DB25250C2}.Release|Any CPU.Build.0 = Release|Any CPU
{FB2D306B-1042-4A70-31ED-F991A1599371}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{FB2D306B-1042-4A70-31ED-F991A1599371}.Debug|Any CPU.Build.0 = Debug|Any CPU
{FB2D306B-1042-4A70-31ED-F991A1599371}.Release|Any CPU.ActiveCfg = Release|Any CPU
{FB2D306B-1042-4A70-31ED-F991A1599371}.Release|Any CPU.Build.0 = Release|Any CPU
EndGlobalSection EndGlobalSection
GlobalSection(SolutionProperties) = preSolution GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE HideSolutionNode = FALSE
@@ -116,8 +116,8 @@ Global
{211619F5-AE25-4BA5-A552-BACAFE0632D3} = {9943209E-1744-4944-B1BA-4F87FC1A0EEB} {211619F5-AE25-4BA5-A552-BACAFE0632D3} = {9943209E-1744-4944-B1BA-4F87FC1A0EEB}
{224C4845-1CDE-22B7-F3A9-1FF9297F70E8} = {0CBC2432-A561-4440-89BC-671B66A24146} {224C4845-1CDE-22B7-F3A9-1FF9297F70E8} = {0CBC2432-A561-4440-89BC-671B66A24146}
{EC768913-6270-14F4-1DD3-69C87A659462} = {E3C758DC-914D-4B7E-8457-0813F1FDB0CB} {EC768913-6270-14F4-1DD3-69C87A659462} = {E3C758DC-914D-4B7E-8457-0813F1FDB0CB}
{FB2D306B-1042-4A70-31ED-F991A1599371} = {E3C758DC-914D-4B7E-8457-0813F1FDB0CB}
{5DCCF9A1-C03F-90E6-87D3-E96DB25250C2} = {E3C758DC-914D-4B7E-8457-0813F1FDB0CB} {5DCCF9A1-C03F-90E6-87D3-E96DB25250C2} = {E3C758DC-914D-4B7E-8457-0813F1FDB0CB}
{FB2D306B-1042-4A70-31ED-F991A1599371} = {E3C758DC-914D-4B7E-8457-0813F1FDB0CB}
EndGlobalSection EndGlobalSection
GlobalSection(ExtensibilityGlobals) = postSolution GlobalSection(ExtensibilityGlobals) = postSolution
SolutionGuid = {73E60370-756D-45AD-A19A-C40A02DACCC7} SolutionGuid = {73E60370-756D-45AD-A19A-C40A02DACCC7}