Compare commits

14 Commits

Author SHA1 Message Date
OlgunR
e6b41f10c8 Merge branch 'feat/receiver-ui' of http://git.dd:3000/AppStd/EnvelopeGenerator into feat/receiver-ui 2025-12-10 11:16:27 +01:00
OlgunR
dc78ad4a24 Improve canvas mouse position accuracy; add settings.json
Refactored mouse event handling in pdfInterop.js to use a new getPos helper, ensuring accurate coordinate mapping on scaled or resized canvases. Updated start and move functions to use this helper. Added an empty settings.json file.
2025-12-10 11:16:13 +01:00
b282318298 Add launchSettings.json for ReceiverUIBlazor project
Configured development launch profile with browser launch, environment variable, and HTTP/HTTPS URLs for EnvelopeGenerator.ReceiverUIBlazor.
2025-12-10 09:55:32 +01:00
8d68ea8c57 Add IIS publish profile for EnvelopeGenerator.ReceiverUIBlazor
Introduced IISProfile.pubxml to enable packaging and deployment
of the EnvelopeGenerator.ReceiverUIBlazor project to IIS. The
profile configures publish method, build settings, output
package location, and IIS app path.
2025-12-10 09:55:24 +01:00
OlgunR
7cb8b02b1d Revamp UI colors and fonts for warmer, modern look
Switched to "Red Hat Text" and "Teko" fonts via Google Fonts. Updated color palette from cool blues to warm reds and yellows, including backgrounds, accents, and error states. Refreshed button, top bar, and card styles for improved visual hierarchy and softer appearance.
2025-12-09 10:58:53 +01:00
OlgunR
cc1d33462c Add drag-and-drop PDF support to Blazor app
Implement drag-and-drop PDF loading via JS interop and DotNetObjectReference. Refactor file loading logic and UI structure for clarity. Add IAsyncDisposable for resource cleanup. Update pdfInterop.js to handle drop events and send PDF data to Blazor.
2025-12-09 10:42:11 +01:00
OlgunR
cd85b4fffc Improve PDF reset logic and modernize UI styling
Refactor PDF reset to restore original document state using a new OriginalPdfBase64 variable and async logic. Redesign app.css with a lighter color palette, CSS variables, and updated styles for buttons, overlays, modals, and inputs for a cleaner, more accessible UI. Adjust signature and text overlay colors in pdfInterop.js for better contrast and consistency.
2025-12-09 10:26:01 +01:00
OlgunR
ab3e7fb4e9 Error while dragging fields fixed 2025-12-09 09:50:10 +01:00
OlgunR
4a7676765c Fixed error while loading file 2025-12-09 08:57:39 +01:00
OlgunR
562ceb9c3f First results converting receiver-ui-react into a Blazor Web App 2025-12-08 16:21:10 +01:00
OlgunR
751ea706df Created EnvelopeGenerator.ReceiverUIBlazor 2025-12-08 16:06:26 +01:00
OlgunR
490ad9f7cf Created EnvelopeGenerator.ReceiverUIBlazor 2025-12-08 16:04:51 +01:00
7aeaba7c12 Add EnvelopeGenerator.ReceiverUI project and UI components
Introduced a new Razor Components-based web application, `EnvelopeGenerator.ReceiverUI`, to the solution. This includes:

- Added `EnvelopeGenerator.ReceiverUI` project to `EnvelopeGenerator.sln` with Debug and Release configurations.
- Created `EnvelopeGenerator.ReceiverUI.csproj` targeting `.NET 8.0` with nullable and implicit usings enabled.
- Added Razor components (`App.razor`, `Routes.razor`, `MainLayout.razor`, `NavMenu.razor`, `Counter.razor`, `Error.razor`, `Home.razor`, `Weather.razor`) for layout, navigation, and pages.
- Included styles (`MainLayout.razor.css`, `NavMenu.razor.css`, `app.css`) and assets (`favicon.png`, `bootstrap.min.css`).
- Configured logging and allowed hosts in `appsettings.json` and `appsettings.Development.json`.
- Added `Program.cs` to configure and run the application.
- Added `launchSettings.json` for development and debugging profiles.

These changes enable a modern, interactive web interface with Bootstrap styling and development-friendly configurations.
2025-12-05 09:08:57 +01:00
2301a81a1c init receiver ui with react 2025-12-04 18:11:47 +01:00
112 changed files with 14478 additions and 1494 deletions

1
.vscode/settings.json vendored Normal file
View File

@@ -0,0 +1 @@
{}

View File

@@ -74,11 +74,6 @@ public record EnvelopeDto
/// </summary>
public int? EnvelopeTypeId { get; set; }
/// <summary>
///
/// </summary>
public bool ReadOnly => EnvelopeTypeId == 2;
/// <summary>
///
/// </summary>

View File

@@ -24,7 +24,7 @@ public record DocSignedNotification(EnvelopeReceiverDto Original) : EnvelopeRece
/// <summary>
///
/// </summary>
public PsPdfKitAnnotation? PsPdfKitAnnotation { get; init; }
public PsPdfKitAnnotation PsPdfKitAnnotation { get; init; } = null!;
/// <summary>
///
@@ -59,7 +59,7 @@ public static class DocSignedNotificationExtensions
/// <param name="dtoTask"></param>
/// <param name="psPdfKitAnnotation"></param>
/// <returns></returns>
public static async Task<DocSignedNotification?> ToDocSignedNotification(this Task<EnvelopeReceiverDto?> dtoTask, PsPdfKitAnnotation? psPdfKitAnnotation)
public static async Task<DocSignedNotification?> ToDocSignedNotification(this Task<EnvelopeReceiverDto?> dtoTask, PsPdfKitAnnotation psPdfKitAnnotation)
=> await dtoTask is EnvelopeReceiverDto dto ? new(dto) { PsPdfKitAnnotation = psPdfKitAnnotation } : null;
/// <summary>

View File

@@ -29,9 +29,6 @@ public class AnnotationHandler : INotificationHandler<DocSignedNotification>
/// <param name="notification"></param>
/// <param name="cancel"></param>
/// <returns></returns>
public async Task Handle(DocSignedNotification notification, CancellationToken cancel)
{
if (notification.PsPdfKitAnnotation is PsPdfKitAnnotation annot)
await _repo.CreateAsync(annot.Structured, cancel);
}
public Task Handle(DocSignedNotification notification, CancellationToken cancel)
=> _repo.CreateAsync(notification.PsPdfKitAnnotation.Structured, cancel);
}

View File

@@ -10,8 +10,6 @@ namespace EnvelopeGenerator.Application.Common.Notifications.DocSigned.Handlers;
/// </summary>
public class DocStatusHandler : INotificationHandler<DocSignedNotification>
{
private const string BlankAnnotationJson = "{}";
private readonly ISender _sender;
/// <summary>
@@ -35,9 +33,7 @@ public class DocStatusHandler : INotificationHandler<DocSignedNotification>
{
Envelope = new() { Id = notification.EnvelopeId },
Receiver = new() { Id = notification.ReceiverId},
Value = notification.PsPdfKitAnnotation is PsPdfKitAnnotation annot
? JsonSerializer.Serialize(annot.Instant, Format.Json.ForAnnotations)
: BlankAnnotationJson
Value = JsonSerializer.Serialize(notification.PsPdfKitAnnotation.Instant, Format.Json.ForAnnotations)
}, cancel);
}
}

View File

@@ -14,7 +14,7 @@
<ItemGroup>
<PackageReference Include="Dapper" Version="2.1.66" />
<PackageReference Include="DigitalData.Core.Abstraction.Application" Version="1.6.0" />
<PackageReference Include="DigitalData.Core.Abstraction.Application" Version="1.4.0" />
<PackageReference Include="DigitalData.Core.Application" Version="3.4.0" />
<PackageReference Include="DigitalData.Core.Client" Version="2.1.0" />
<PackageReference Include="DigitalData.Core.Exceptions" Version="1.1.0" />

View File

@@ -70,8 +70,8 @@
<Reference Include="DigitalData.Controls.DocumentViewer, Version=1.9.8.0, Culture=neutral, processorArchitecture=MSIL">
<HintPath>..\packages\DigitalData.Controls.DocumentViewer.1.9.8\lib\net462\DigitalData.Controls.DocumentViewer.dll</HintPath>
</Reference>
<Reference Include="DigitalData.Core.Abstraction.Application, Version=1.6.0.0, Culture=neutral, processorArchitecture=MSIL">
<HintPath>..\packages\DigitalData.Core.Abstraction.Application.1.6.0\lib\net462\DigitalData.Core.Abstraction.Application.dll</HintPath>
<Reference Include="DigitalData.Core.Abstraction.Application, Version=1.4.0.0, Culture=neutral, processorArchitecture=MSIL">
<HintPath>..\packages\DigitalData.Core.Abstraction.Application.1.4.0\lib\net462\DigitalData.Core.Abstraction.Application.dll</HintPath>
</Reference>
<Reference Include="DigitalData.Core.Abstractions, Version=4.3.0.0, Culture=neutral, processorArchitecture=MSIL">
<HintPath>..\packages\DigitalData.Core.Abstractions.4.3.0\lib\net462\DigitalData.Core.Abstractions.dll</HintPath>
@@ -109,6 +109,9 @@
<Reference Include="EntityFramework.SqlServer, Version=6.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089, processorArchitecture=MSIL">
<HintPath>..\packages\EntityFramework.6.5.1\lib\net45\EntityFramework.SqlServer.dll</HintPath>
</Reference>
<Reference Include="EnvelopeGenerator.CommonServices">
<HintPath>..\EnvelopeGenerator.CommonServices\bin\Debug\EnvelopeGenerator.CommonServices.dll</HintPath>
</Reference>
<Reference Include="FirebirdSql.Data.FirebirdClient, Version=7.5.0.0, Culture=neutral, PublicKeyToken=3750abcc3150b00c, processorArchitecture=MSIL">
<HintPath>..\packages\FirebirdSql.Data.FirebirdClient.7.5.0\lib\net452\FirebirdSql.Data.FirebirdClient.dll</HintPath>
</Reference>
@@ -463,12 +466,8 @@
<Project>{6EA0C51F-C2B1-4462-8198-3DE0B32B74F8}</Project>
<Name>EnvelopeGenerator.CommonServices</Name>
</ProjectReference>
<ProjectReference Include="..\EnvelopeGenerator.CommonServices\EnvelopeGenerator.CommonServices.vbproj">
<Project>{6ea0c51f-c2b1-4462-8198-3de0b32b74f8}</Project>
<Name>EnvelopeGenerator.CommonServices</Name>
</ProjectReference>
<ProjectReference Include="..\EnvelopeGenerator.Domain\EnvelopeGenerator.Domain.csproj">
<Project>{4f32a98d-e6f0-4a09-bd97-1cf26107e837}</Project>
<Project>{4F32A98D-E6F0-4A09-BD97-1CF26107E837}</Project>
<Name>EnvelopeGenerator.Domain</Name>
</ProjectReference>
<ProjectReference Include="..\EnvelopeGenerator.Infrastructure\EnvelopeGenerator.Infrastructure.csproj">
@@ -496,9 +495,6 @@
<Content Include="MailLicense.xml" />
<Content Include="README.txt" />
</ItemGroup>
<ItemGroup>
<WCFMetadata Include="Connected Services\" />
</ItemGroup>
<Import Project="$(MSBuildToolsPath)\Microsoft.VisualBasic.targets" />
<Import Project="..\packages\GdPicture.runtimes.windows.14.3.3\build\net462\GdPicture.runtimes.windows.targets" Condition="Exists('..\packages\GdPicture.runtimes.windows.14.3.3\build\net462\GdPicture.runtimes.windows.targets')" />
<Target Name="EnsureNuGetPackageBuildImports" BeforeTargets="PrepareForBuild">

View File

@@ -3,7 +3,7 @@
<package id="AutoMapper" version="10.1.1" targetFramework="net462" />
<package id="BouncyCastle.Cryptography" version="2.5.0" targetFramework="net462" />
<package id="DigitalData.Controls.DocumentViewer" version="1.9.8" targetFramework="net462" />
<package id="DigitalData.Core.Abstraction.Application" version="1.6.0" targetFramework="net462" />
<package id="DigitalData.Core.Abstraction.Application" version="1.4.0" targetFramework="net462" />
<package id="DigitalData.Core.Abstractions" version="4.3.0" targetFramework="net462" />
<package id="DigitalData.Modules.Base" version="1.3.8" targetFramework="net462" />
<package id="DigitalData.Modules.Config" version="1.3.0" targetFramework="net462" />

View File

@@ -72,8 +72,8 @@
<Reference Include="DevExpress.XtraEditors.v21.2, Version=21.2.4.0, Culture=neutral, PublicKeyToken=b88d1754d700e49a" />
<Reference Include="DevExpress.XtraGauges.v21.2.Core, Version=21.2.4.0, Culture=neutral, PublicKeyToken=b88d1754d700e49a, processorArchitecture=MSIL" />
<Reference Include="DevExpress.XtraReports.v21.2, Version=21.2.4.0, Culture=neutral, PublicKeyToken=b88d1754d700e49a, processorArchitecture=MSIL" />
<Reference Include="DigitalData.Core.Abstraction.Application, Version=1.6.0.0, Culture=neutral, processorArchitecture=MSIL">
<HintPath>..\packages\DigitalData.Core.Abstraction.Application.1.6.0\lib\net462\DigitalData.Core.Abstraction.Application.dll</HintPath>
<Reference Include="DigitalData.Core.Abstraction.Application, Version=1.4.0.0, Culture=neutral, processorArchitecture=MSIL">
<HintPath>..\packages\DigitalData.Core.Abstraction.Application.1.4.0\lib\net462\DigitalData.Core.Abstraction.Application.dll</HintPath>
</Reference>
<Reference Include="DigitalData.Core.Abstractions, Version=4.3.0.0, Culture=neutral, processorArchitecture=MSIL">
<HintPath>..\packages\DigitalData.Core.Abstractions.4.3.0\lib\net462\DigitalData.Core.Abstractions.dll</HintPath>

View File

@@ -422,7 +422,7 @@ Namespace Jobs
End Function
Private Function GetEnvelopeData(pEnvelopeId As Integer) As EnvelopeData
Dim oSql = $"SELECT T.GUID, T.ENVELOPE_UUID, T.ENVELOPE_TYPE, T2.FILEPATH, T2.BYTE_DATA FROM [dbo].[TBSIG_ENVELOPE] T
Dim oSql = $"SELECT T.GUID, T.ENVELOPE_UUID,T2.FILEPATH, T2.BYTE_DATA FROM [dbo].[TBSIG_ENVELOPE] T
JOIN TBSIG_ENVELOPE_DOCUMENT T2 ON T.GUID = T2.ENVELOPE_ID
WHERE T.GUID = {pEnvelopeId}"
Dim oTable As DataTable = Database.GetDatatable(oSql)

View File

@@ -37,15 +37,6 @@ Namespace Jobs.FinalizeDocument
Public Function BurnAnnotsToPDF(pSourceBuffer As Byte(), pInstantJSONList As List(Of String), envelopeId As Integer) As Byte()
'read the elements of envelope with their annotations
Using scope = Factory.Shared.ScopeFactory.CreateScope()
Dim envRepo = scope.ServiceProvider.Repository(Of Envelope)()
Dim envelope = envRepo.Where(Function(env) env.Id = envelopeId).FirstOrDefault()
If envelope Is Nothing Then
Throw New BurnAnnotationException($"Envelope with Id {envelopeId} not found.")
ElseIf envelope.ReadOnly Then
Return pSourceBuffer
End If
Dim sigRepo = scope.ServiceProvider.Repository(Of Signature)()
Dim elements = sigRepo _
.Where(Function(sig) sig.Document.EnvelopeId = envelopeId) _

View File

@@ -2,7 +2,7 @@
<packages>
<package id="AutoMapper" version="10.1.1" targetFramework="net462" />
<package id="BouncyCastle.Cryptography" version="2.5.0" targetFramework="net462" />
<package id="DigitalData.Core.Abstraction.Application" version="1.6.0" targetFramework="net462" />
<package id="DigitalData.Core.Abstraction.Application" version="1.4.0" targetFramework="net462" />
<package id="DigitalData.Core.Abstractions" version="4.3.0" targetFramework="net462" />
<package id="DigitalData.Modules.Base" version="1.3.8" targetFramework="net462" />
<package id="DigitalData.Modules.Config" version="1.3.0" targetFramework="net462" />

View File

@@ -2,7 +2,6 @@
using System.ComponentModel.DataAnnotations;
using System.ComponentModel.DataAnnotations.Schema;
using EnvelopeGenerator.Domain.Constants;
using Newtonsoft.Json;
#if NETFRAMEWORK
using System;
@@ -108,10 +107,6 @@ public class Envelope
[Column("ENVELOPE_TYPE")]
public int? EnvelopeTypeId { get; set; }
[JsonIgnore]
[NotMapped]
public bool ReadOnly => EnvelopeTypeId == 2;
[Column("CERTIFICATION_TYPE")]
public int? CertificationType { get; set; }

View File

@@ -12,7 +12,7 @@ namespace EnvelopeGenerator.Infrastructure
public EGDbContext CreateDbContext(string[] args)
{
var config = new ConfigurationBuilder()
.SetBasePath(Directory.GetCurrentDirectory())
.SetBasePath(Directory.GetCurrentDirectory()) // Önemli!
.AddJsonFile("appsettings.migration.json")
.Build();

View File

@@ -22,8 +22,8 @@
<ItemGroup>
<PackageReference Include="Dapper" Version="2.1.66" />
<PackageReference Include="DigitalData.Core.Abstraction.Application" Version="1.6.0" />
<PackageReference Include="DigitalData.Core.Infrastructure" Version="2.6.1" />
<PackageReference Include="DigitalData.Core.Abstraction.Application" Version="1.4.0" />
<PackageReference Include="DigitalData.Core.Infrastructure" Version="2.4.5" />
<PackageReference Include="QuestPDF" Version="2025.7.1" />
</ItemGroup>

View File

@@ -1,22 +0,0 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net8.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Quartz" Version="3.9.0" />
<PackageReference Include="Newtonsoft.Json" Version="13.0.3" />
<PackageReference Include="Microsoft.Extensions.Logging.Abstractions" Version="8.0.2" />
<PackageReference Include="Microsoft.Data.SqlClient" Version="6.0.2" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\EnvelopeGenerator.Domain\EnvelopeGenerator.Domain.csproj" />
<ProjectReference Include="..\EnvelopeGenerator.Infrastructure\EnvelopeGenerator.Infrastructure.csproj" />
<ProjectReference Include="..\EnvelopeGenerator.PdfEditor\EnvelopeGenerator.PdfEditor.csproj" />
</ItemGroup>
</Project>

View File

@@ -1,151 +0,0 @@
using System.Collections.Generic;
using System.Data;
using System.Threading.Tasks;
using EnvelopeGenerator.Domain.Constants;
using Microsoft.Data.SqlClient;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Logging.Abstractions;
using Quartz;
namespace EnvelopeGenerator.Jobs.APIBackendJobs;
public class APIEnvelopeJob(ILogger<APIEnvelopeJob>? logger = null) : IJob
{
private readonly ILogger<APIEnvelopeJob> _logger = logger ?? NullLogger<APIEnvelopeJob>.Instance;
public async Task Execute(IJobExecutionContext context)
{
var jobId = context.JobDetail.Key.ToString();
_logger.LogDebug("API Envelopes - Starting job {JobId}", jobId);
try
{
var connectionString = context.MergedJobDataMap.GetString(Value.DATABASE);
if (string.IsNullOrWhiteSpace(connectionString))
{
_logger.LogWarning("API Envelopes - Connection string missing");
return;
}
await using var connection = new SqlConnection(connectionString);
await connection.OpenAsync(context.CancellationToken);
await ProcessInvitationsAsync(connection, context.CancellationToken);
await ProcessWithdrawnAsync(connection, context.CancellationToken);
_logger.LogDebug("API Envelopes - Completed job {JobId} successfully", jobId);
}
catch (System.Exception ex)
{
_logger.LogError(ex, "API Envelopes job failed");
}
finally
{
_logger.LogDebug("API Envelopes execution for {JobId} ended", jobId);
}
}
private async Task ProcessInvitationsAsync(SqlConnection connection, System.Threading.CancellationToken cancellationToken)
{
const string sql = "SELECT GUID FROM TBSIG_ENVELOPE WHERE SOURCE = 'API' AND STATUS = 1003 ORDER BY GUID";
var envelopeIds = new List<int>();
await using (var command = new SqlCommand(sql, connection))
await using (var reader = await command.ExecuteReaderAsync(cancellationToken))
{
while (await reader.ReadAsync(cancellationToken))
{
if (reader[0] is int id)
{
envelopeIds.Add(id);
}
}
}
if (envelopeIds.Count == 0)
{
_logger.LogDebug("SendInvMail - No envelopes found");
return;
}
_logger.LogInformation("SendInvMail - Found {Count} envelopes", envelopeIds.Count);
var total = envelopeIds.Count;
var current = 1;
foreach (var id in envelopeIds)
{
_logger.LogInformation("SendInvMail - Processing Envelope {EnvelopeId} ({Current}/{Total})", id, current, total);
try
{
// Placeholder for invitation email sending logic.
_logger.LogDebug("SendInvMail - Marking envelope {EnvelopeId} as queued", id);
const string updateSql = "UPDATE TBSIG_ENVELOPE SET CURRENT_WORK_APP = @App WHERE GUID = @Id";
await using var updateCommand = new SqlCommand(updateSql, connection);
updateCommand.Parameters.AddWithValue("@App", "signFLOW_API_EnvJob_InvMail");
updateCommand.Parameters.AddWithValue("@Id", id);
await updateCommand.ExecuteNonQueryAsync(cancellationToken);
}
catch (System.Exception ex)
{
_logger.LogWarning(ex, "SendInvMail - Unhandled exception while working envelope {EnvelopeId}", id);
}
current++;
_logger.LogInformation("SendInvMail - Envelope finalized");
}
}
private async Task ProcessWithdrawnAsync(SqlConnection connection, System.Threading.CancellationToken cancellationToken)
{
const string sql = @"SELECT ENV.GUID, REJ.COMMENT AS REJECTION_REASON FROM
(SELECT * FROM TBSIG_ENVELOPE WHERE STATUS = 1009 AND SOURCE = 'API') ENV INNER JOIN
(SELECT MAX(GUID) GUID, ENVELOPE_ID, MAX(ADDED_WHEN) ADDED_WHEN, MAX(ACTION_DATE) ACTION_DATE, COMMENT FROM TBSIG_ENVELOPE_HISTORY WHERE STATUS = 1009 GROUP BY ENVELOPE_ID, COMMENT ) REJ ON ENV.GUID = REJ.ENVELOPE_ID LEFT JOIN
(SELECT * FROM TBSIG_ENVELOPE_HISTORY WHERE STATUS = 3004 ) M_Send ON ENV.GUID = M_Send.ENVELOPE_ID
WHERE M_Send.GUID IS NULL";
var withdrawn = new List<(int EnvelopeId, string Reason)>();
await using (var command = new SqlCommand(sql, connection))
await using (var reader = await command.ExecuteReaderAsync(cancellationToken))
{
while (await reader.ReadAsync(cancellationToken))
{
var id = reader.GetInt32(0);
var reason = reader.IsDBNull(1) ? string.Empty : reader.GetString(1);
withdrawn.Add((id, reason));
}
}
if (withdrawn.Count == 0)
{
_logger.LogDebug("WithdrawnEnv - No envelopes found");
return;
}
_logger.LogInformation("WithdrawnEnv - Found {Count} envelopes", withdrawn.Count);
var total = withdrawn.Count;
var current = 1;
foreach (var (envelopeId, reason) in withdrawn)
{
_logger.LogInformation("WithdrawnEnv - Processing Envelope {EnvelopeId} ({Current}/{Total})", envelopeId, current, total);
try
{
// Log withdrawn mail trigger placeholder
const string insertHistory = "INSERT INTO TBSIG_ENVELOPE_HISTORY (ENVELOPE_ID, STATUS, USER_REFERENCE, ADDED_WHEN, ACTION_DATE, COMMENT) VALUES (@EnvelopeId, @Status, @UserReference, GETDATE(), GETDATE(), @Comment)";
await using var insertCommand = new SqlCommand(insertHistory, connection);
insertCommand.Parameters.AddWithValue("@EnvelopeId", envelopeId);
insertCommand.Parameters.AddWithValue("@Status", 3004);
insertCommand.Parameters.AddWithValue("@UserReference", "API");
insertCommand.Parameters.AddWithValue("@Comment", reason ?? string.Empty);
await insertCommand.ExecuteNonQueryAsync(cancellationToken);
}
catch (System.Exception ex)
{
_logger.LogWarning(ex, "WithdrawnEnv - Unhandled exception while working envelope {EnvelopeId}", envelopeId);
}
current++;
_logger.LogInformation("WithdrawnEnv - Envelope finalized");
}
}
}

View File

@@ -1,30 +0,0 @@
using System;
using System.Data;
namespace EnvelopeGenerator.Jobs;
public static class DataRowExtensions
{
public static T? GetValueOrDefault<T>(this DataRow row, string columnName, T? defaultValue = default)
{
if (!row.Table.Columns.Contains(columnName))
{
return defaultValue;
}
var value = row[columnName];
if (value == DBNull.Value)
{
return defaultValue;
}
try
{
return (T)Convert.ChangeType(value, typeof(T));
}
catch
{
return defaultValue;
}
}
}

View File

@@ -1,28 +0,0 @@
namespace EnvelopeGenerator.Jobs.FinalizeDocument;
public static class FinalizeDocumentExceptions
{
public class MergeDocumentException : ApplicationException
{
public MergeDocumentException(string message) : base(message) { }
public MergeDocumentException(string message, Exception innerException) : base(message, innerException) { }
}
public class BurnAnnotationException : ApplicationException
{
public BurnAnnotationException(string message) : base(message) { }
public BurnAnnotationException(string message, Exception innerException) : base(message, innerException) { }
}
public class CreateReportException : ApplicationException
{
public CreateReportException(string message) : base(message) { }
public CreateReportException(string message, Exception innerException) : base(message, innerException) { }
}
public class ExportDocumentException : ApplicationException
{
public ExportDocumentException(string message) : base(message) { }
public ExportDocumentException(string message, Exception innerException) : base(message, innerException) { }
}
}

View File

@@ -1,229 +0,0 @@
using System.Collections.Generic;
using System.Data;
using System.IO;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using EnvelopeGenerator.Domain.Constants;
using EnvelopeGenerator.Domain.Entities;
using Microsoft.Data.SqlClient;
using Microsoft.Extensions.Logging;
using Quartz;
using static EnvelopeGenerator.Jobs.FinalizeDocument.FinalizeDocumentExceptions;
namespace EnvelopeGenerator.Jobs.FinalizeDocument;
public class FinalizeDocumentJob : IJob
{
private readonly ILogger<FinalizeDocumentJob> _logger;
private readonly PDFBurner _pdfBurner;
private readonly PDFMerger _pdfMerger;
private readonly ReportCreator _reportCreator;
private record ConfigSettings(string DocumentPath, string DocumentPathOrigin, string ExportPath);
public FinalizeDocumentJob(
ILogger<FinalizeDocumentJob> logger,
PDFBurner pdfBurner,
PDFMerger pdfMerger,
ReportCreator reportCreator)
{
_logger = logger;
_pdfBurner = pdfBurner;
_pdfMerger = pdfMerger;
_reportCreator = reportCreator;
}
public async Task Execute(IJobExecutionContext context)
{
var jobId = context.JobDetail.Key.ToString();
_logger.LogDebug("Starting job {JobId}", jobId);
try
{
var connectionString = context.MergedJobDataMap.GetString(Value.DATABASE);
if (string.IsNullOrWhiteSpace(connectionString))
{
_logger.LogWarning("FinalizeDocument - Connection string missing");
return;
}
await using var connection = new SqlConnection(connectionString);
await connection.OpenAsync(context.CancellationToken);
var config = await LoadConfigurationAsync(connection, context.CancellationToken);
var envelopes = await LoadCompletedEnvelopesAsync(connection, context.CancellationToken);
if (envelopes.Count == 0)
{
_logger.LogInformation("No completed envelopes found");
return;
}
var total = envelopes.Count;
var current = 1;
foreach (var envelopeId in envelopes)
{
_logger.LogInformation("Finalizing Envelope {EnvelopeId} ({Current}/{Total})", envelopeId, current, total);
try
{
var envelopeData = await GetEnvelopeDataAsync(connection, envelopeId, context.CancellationToken);
if (envelopeData is null)
{
_logger.LogWarning("Envelope data not found for {EnvelopeId}", envelopeId);
continue;
}
var data = envelopeData.Value;
var envelope = new Envelope
{
Id = envelopeId,
Uuid = data.EnvelopeUuid ?? string.Empty,
Title = data.Title ?? string.Empty,
FinalEmailToCreator = (int)FinalEmailType.No,
FinalEmailToReceivers = (int)FinalEmailType.No
};
var burned = _pdfBurner.BurnAnnotsToPDF(data.DocumentBytes, data.AnnotationData, envelopeId);
var report = _reportCreator.CreateReport(connection, envelope);
var merged = _pdfMerger.MergeDocuments(burned, report);
var outputDirectory = Path.Combine(config.ExportPath, data.ParentFolderUid);
Directory.CreateDirectory(outputDirectory);
var outputPath = Path.Combine(outputDirectory, $"{envelope.Uuid}.pdf");
await File.WriteAllBytesAsync(outputPath, merged, context.CancellationToken);
await UpdateDocumentResultAsync(connection, envelopeId, merged, context.CancellationToken);
await ArchiveEnvelopeAsync(connection, envelopeId, context.CancellationToken);
}
catch (MergeDocumentException ex)
{
_logger.LogWarning(ex, "Certificate Document job failed at merging documents");
}
catch (ExportDocumentException ex)
{
_logger.LogWarning(ex, "Certificate Document job failed at exporting document");
}
catch (Exception ex)
{
_logger.LogError(ex, "Unhandled exception while working envelope {EnvelopeId}", envelopeId);
}
current++;
_logger.LogInformation("Envelope {EnvelopeId} finalized", envelopeId);
}
_logger.LogDebug("Completed job {JobId} successfully", jobId);
}
catch (Exception ex)
{
_logger.LogError(ex, "Certificate Document job failed");
}
finally
{
_logger.LogDebug("Job execution for {JobId} ended", jobId);
}
}
private async Task<ConfigSettings> LoadConfigurationAsync(SqlConnection connection, CancellationToken cancellationToken)
{
const string sql = "SELECT TOP 1 DOCUMENT_PATH, EXPORT_PATH FROM TBSIG_CONFIG";
await using var command = new SqlCommand(sql, connection);
await using var reader = await command.ExecuteReaderAsync(cancellationToken);
if (await reader.ReadAsync(cancellationToken))
{
var documentPath = reader.IsDBNull(0) ? string.Empty : reader.GetString(0);
var exportPath = reader.IsDBNull(1) ? string.Empty : reader.GetString(1);
return new ConfigSettings(documentPath, documentPath, exportPath);
}
return new ConfigSettings(string.Empty, string.Empty, Path.GetTempPath());
}
private async Task<List<int>> LoadCompletedEnvelopesAsync(SqlConnection connection, CancellationToken cancellationToken)
{
const string sql = "SELECT GUID FROM TBSIG_ENVELOPE WHERE STATUS = @Status AND DATEDIFF(minute, CHANGED_WHEN, GETDATE()) >= 1 ORDER BY GUID";
var ids = new List<int>();
await using var command = new SqlCommand(sql, connection);
command.Parameters.AddWithValue("@Status", (int)EnvelopeStatus.EnvelopeCompletelySigned);
await using var reader = await command.ExecuteReaderAsync(cancellationToken);
while (await reader.ReadAsync(cancellationToken))
{
ids.Add(reader.GetInt32(0));
}
return ids;
}
private async Task<(int EnvelopeId, string? EnvelopeUuid, string? Title, byte[] DocumentBytes, List<string> AnnotationData, string ParentFolderUid)?> GetEnvelopeDataAsync(SqlConnection connection, int envelopeId, CancellationToken cancellationToken)
{
const string sql = @"SELECT T.GUID, T.ENVELOPE_UUID, T.TITLE, T2.FILEPATH, T2.BYTE_DATA FROM [dbo].[TBSIG_ENVELOPE] T
JOIN TBSIG_ENVELOPE_DOCUMENT T2 ON T.GUID = T2.ENVELOPE_ID
WHERE T.GUID = @EnvelopeId";
await using var command = new SqlCommand(sql, connection);
command.Parameters.AddWithValue("@EnvelopeId", envelopeId);
await using var reader = await command.ExecuteReaderAsync(CommandBehavior.SingleRow, cancellationToken);
if (!await reader.ReadAsync(cancellationToken))
{
return null;
}
var envelopeUuid = reader.IsDBNull(1) ? string.Empty : reader.GetString(1);
var title = reader.IsDBNull(2) ? string.Empty : reader.GetString(2);
var filePath = reader.IsDBNull(3) ? string.Empty : reader.GetString(3);
var bytes = reader.IsDBNull(4) ? Array.Empty<byte>() : (byte[])reader[4];
await reader.CloseAsync();
if (bytes.Length == 0 && !string.IsNullOrWhiteSpace(filePath) && File.Exists(filePath))
{
bytes = await File.ReadAllBytesAsync(filePath, cancellationToken);
}
var annotations = await GetAnnotationDataAsync(connection, envelopeId, cancellationToken);
var parentFolderUid = !string.IsNullOrWhiteSpace(filePath)
? Path.GetFileName(Path.GetDirectoryName(filePath) ?? string.Empty)
: envelopeUuid;
return (envelopeId, envelopeUuid, title, bytes, annotations, parentFolderUid ?? envelopeUuid ?? envelopeId.ToString());
}
private async Task<List<string>> GetAnnotationDataAsync(SqlConnection connection, int envelopeId, CancellationToken cancellationToken)
{
const string sql = "SELECT VALUE FROM TBSIG_DOCUMENT_STATUS WHERE ENVELOPE_ID = @EnvelopeId";
var result = new List<string>();
await using var command = new SqlCommand(sql, connection);
command.Parameters.AddWithValue("@EnvelopeId", envelopeId);
await using var reader = await command.ExecuteReaderAsync(cancellationToken);
while (await reader.ReadAsync(cancellationToken))
{
if (!reader.IsDBNull(0))
{
result.Add(reader.GetString(0));
}
}
return result;
}
private static async Task UpdateDocumentResultAsync(SqlConnection connection, int envelopeId, byte[] bytes, CancellationToken cancellationToken)
{
const string sql = "UPDATE TBSIG_ENVELOPE SET DOC_RESULT = @ImageData WHERE GUID = @EnvelopeId";
await using var command = new SqlCommand(sql, connection);
command.Parameters.AddWithValue("@ImageData", bytes);
command.Parameters.AddWithValue("@EnvelopeId", envelopeId);
await command.ExecuteNonQueryAsync(cancellationToken);
}
private static async Task ArchiveEnvelopeAsync(SqlConnection connection, int envelopeId, CancellationToken cancellationToken)
{
const string sql = "UPDATE TBSIG_ENVELOPE SET STATUS = @Status, CHANGED_WHEN = GETDATE() WHERE GUID = @EnvelopeId";
await using var command = new SqlCommand(sql, connection);
command.Parameters.AddWithValue("@Status", (int)EnvelopeStatus.EnvelopeArchived);
command.Parameters.AddWithValue("@EnvelopeId", envelopeId);
await command.ExecuteNonQueryAsync(cancellationToken);
}
}

View File

@@ -1,277 +0,0 @@
using System.Collections.Generic;
using System.Drawing;
using System.Linq;
using iText.IO.Image;
using iText.Kernel.Colors;
using iText.Kernel.Pdf;
using iText.Kernel.Pdf.Canvas;
using iText.Layout;
using iText.Layout.Element;
using iText.Layout.Font;
using iText.Layout.Properties;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Logging.Abstractions;
using Newtonsoft.Json;
using static EnvelopeGenerator.Jobs.FinalizeDocument.FinalizeDocumentExceptions;
using LayoutImage = iText.Layout.Element.Image;
namespace EnvelopeGenerator.Jobs.FinalizeDocument;
public class PDFBurner
{
private static readonly FontProvider FontProvider = CreateFontProvider();
private readonly ILogger<PDFBurner> _logger;
private readonly PDFBurnerParams _pdfBurnerParams;
public PDFBurner() : this(NullLogger<PDFBurner>.Instance, new PDFBurnerParams())
{
}
public PDFBurner(ILogger<PDFBurner> logger, PDFBurnerParams? pdfBurnerParams = null)
{
_logger = logger;
_pdfBurnerParams = pdfBurnerParams ?? new PDFBurnerParams();
}
public byte[] BurnAnnotsToPDF(byte[] sourceBuffer, IList<string> instantJsonList, int envelopeId)
{
if (sourceBuffer is null || sourceBuffer.Length == 0)
{
throw new BurnAnnotationException("Source document is empty");
}
try
{
using var inputStream = new MemoryStream(sourceBuffer);
using var outputStream = new MemoryStream();
using var reader = new PdfReader(inputStream);
using var writer = new PdfWriter(outputStream);
using var pdf = new PdfDocument(reader, writer);
foreach (var json in instantJsonList ?? Enumerable.Empty<string>())
{
if (string.IsNullOrWhiteSpace(json))
{
continue;
}
var annotationData = JsonConvert.DeserializeObject<AnnotationData>(json);
if (annotationData?.annotations is null)
{
continue;
}
annotationData.annotations.Reverse();
foreach (var annotation in annotationData.annotations)
{
try
{
switch (annotation.type)
{
case AnnotationType.Image:
AddImageAnnotation(pdf, annotation, annotationData.attachments);
break;
case AnnotationType.Ink:
AddInkAnnotation(pdf, annotation);
break;
case AnnotationType.Widget:
var formFieldValue = annotationData.formFieldValues?.FirstOrDefault(fv => fv.name == annotation.id);
if (formFieldValue is not null && !_pdfBurnerParams.IgnoredLabels.Contains(formFieldValue.value))
{
AddFormFieldValue(pdf, annotation, formFieldValue.value);
}
break;
}
}
catch (Exception ex)
{
_logger.LogWarning(ex, "Error applying annotation {AnnotationId} on envelope {EnvelopeId}", annotation.id, envelopeId);
throw new BurnAnnotationException("Adding annotation failed", ex);
}
}
}
pdf.Close();
return outputStream.ToArray();
}
catch (BurnAnnotationException)
{
throw;
}
catch (Exception ex)
{
_logger.LogError(ex, "Failed to burn annotations for envelope {EnvelopeId}", envelopeId);
throw new BurnAnnotationException("Annotations could not be burned", ex);
}
}
private void AddImageAnnotation(PdfDocument pdf, Annotation annotation, Dictionary<string, Attachment>? attachments)
{
if (attachments is null || string.IsNullOrWhiteSpace(annotation.imageAttachmentId) || !attachments.TryGetValue(annotation.imageAttachmentId, out var attachment))
{
return;
}
var page = pdf.GetPage(annotation.pageIndex + 1);
var bounds = annotation.bbox.Select(ToInches).ToList();
var x = (float)bounds[0];
var y = (float)bounds[1];
var width = (float)bounds[2];
var height = (float)bounds[3];
var imageBytes = Convert.FromBase64String(attachment.binary);
var imageData = ImageDataFactory.Create(imageBytes);
var image = new LayoutImage(imageData)
.ScaleAbsolute(width, height)
.SetFixedPosition(annotation.pageIndex + 1, x, y);
using var canvas = new Canvas(new PdfCanvas(page), page.GetPageSize());
canvas.Add(image);
}
private void AddInkAnnotation(PdfDocument pdf, Annotation annotation)
{
if (annotation.lines?.points is null)
{
return;
}
var page = pdf.GetPage(annotation.pageIndex + 1);
var canvas = new PdfCanvas(page);
var color = ParseColor(annotation.strokeColor);
canvas.SetStrokeColor(color);
canvas.SetLineWidth(1);
foreach (var segment in annotation.lines.points)
{
var first = true;
foreach (var point in segment)
{
var (px, py) = (ToInches(point[0]), ToInches(point[1]));
if (first)
{
canvas.MoveTo(px, py);
first = false;
}
else
{
canvas.LineTo(px, py);
}
}
canvas.Stroke();
}
}
private static FontProvider CreateFontProvider()
{
var provider = new FontProvider();
provider.AddStandardPdfFonts();
provider.AddSystemFonts();
return provider;
}
private void AddFormFieldValue(PdfDocument pdf, Annotation annotation, string value)
{
var bounds = annotation.bbox.Select(ToInches).ToList();
var x = (float)bounds[0];
var y = (float)bounds[1];
var width = (float)bounds[2];
var height = (float)bounds[3];
var page = pdf.GetPage(annotation.pageIndex + 1);
var canvas = new Canvas(new PdfCanvas(page), page.GetPageSize());
canvas.SetProperty(Property.FONT_PROVIDER, FontProvider);
canvas.SetProperty(Property.FONT, FontProvider.GetFontSet());
var paragraph = new Paragraph(value)
.SetFontSize(_pdfBurnerParams.FontSize)
.SetFontColor(ColorConstants.BLACK)
.SetFontFamily(_pdfBurnerParams.FontName);
if (_pdfBurnerParams.FontStyle.HasFlag(FontStyle.Italic))
{
paragraph.SetItalic();
}
if (_pdfBurnerParams.FontStyle.HasFlag(FontStyle.Bold))
{
paragraph.SetBold();
}
canvas.ShowTextAligned(
paragraph,
x + (float)_pdfBurnerParams.TopMargin,
y + (float)_pdfBurnerParams.YOffset,
annotation.pageIndex + 1,
iText.Layout.Properties.TextAlignment.LEFT,
iText.Layout.Properties.VerticalAlignment.TOP,
0);
}
private static DeviceRgb ParseColor(string? color)
{
if (string.IsNullOrWhiteSpace(color))
{
return new DeviceRgb(0, 0, 0);
}
try
{
var drawingColor = ColorTranslator.FromHtml(color);
return new DeviceRgb(drawingColor.R, drawingColor.G, drawingColor.B);
}
catch
{
return new DeviceRgb(0, 0, 0);
}
}
private static double ToInches(double value) => value / 72d;
private static double ToInches(float value) => value / 72d;
#region Model
private static class AnnotationType
{
public const string Image = "pspdfkit/image";
public const string Ink = "pspdfkit/ink";
public const string Widget = "pspdfkit/widget";
}
private sealed class AnnotationData
{
public List<Annotation>? annotations { get; set; }
public Dictionary<string, Attachment>? attachments { get; set; }
public List<FormFieldValue>? formFieldValues { get; set; }
}
private sealed class Annotation
{
public string id { get; set; } = string.Empty;
public List<double> bbox { get; set; } = new();
public string type { get; set; } = string.Empty;
public string imageAttachmentId { get; set; } = string.Empty;
public Lines? lines { get; set; }
public int pageIndex { get; set; }
public string strokeColor { get; set; } = string.Empty;
public string egName { get; set; } = string.Empty;
}
private sealed class Lines
{
public List<List<List<float>>> points { get; set; } = new();
}
private sealed class Attachment
{
public string binary { get; set; } = string.Empty;
public string contentType { get; set; } = string.Empty;
}
private sealed class FormFieldValue
{
public string name { get; set; } = string.Empty;
public string value { get; set; } = string.Empty;
}
#endregion
}

View File

@@ -1,19 +0,0 @@
using System.Collections.Generic;
using System.Drawing;
namespace EnvelopeGenerator.Jobs.FinalizeDocument;
public class PDFBurnerParams
{
public List<string> IgnoredLabels { get; } = new() { "Date", "Datum", "ZIP", "PLZ", "Place", "Ort", "Position", "Stellung" };
public double TopMargin { get; set; } = 0.1;
public double YOffset { get; set; } = -0.3;
public string FontName { get; set; } = "Arial";
public int FontSize { get; set; } = 8;
public FontStyle FontStyle { get; set; } = FontStyle.Italic;
}

View File

@@ -1,46 +0,0 @@
using System.IO;
using iText.Kernel.Pdf;
using iText.Kernel.Utils;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Logging.Abstractions;
using static EnvelopeGenerator.Jobs.FinalizeDocument.FinalizeDocumentExceptions;
namespace EnvelopeGenerator.Jobs.FinalizeDocument;
public class PDFMerger
{
private readonly ILogger<PDFMerger> _logger;
public PDFMerger() : this(NullLogger<PDFMerger>.Instance)
{
}
public PDFMerger(ILogger<PDFMerger> logger)
{
_logger = logger;
}
public byte[] MergeDocuments(byte[] document, byte[] report)
{
try
{
using var finalStream = new MemoryStream();
using var documentReader = new PdfReader(new MemoryStream(document));
using var reportReader = new PdfReader(new MemoryStream(report));
using var writer = new PdfWriter(finalStream);
using var targetDoc = new PdfDocument(documentReader, writer);
using var reportDoc = new PdfDocument(reportReader);
var merger = new PdfMerger(targetDoc);
merger.Merge(reportDoc, 1, reportDoc.GetNumberOfPages());
targetDoc.Close();
return finalStream.ToArray();
}
catch (Exception ex)
{
_logger.LogError(ex, "Failed to merge PDF documents");
throw new MergeDocumentException("Documents could not be merged", ex);
}
}
}

View File

@@ -1,91 +0,0 @@
using System.Data;
using System.IO;
using EnvelopeGenerator.Domain.Entities;
using iText.Kernel.Pdf;
using iText.Layout.Element;
using Microsoft.Data.SqlClient;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Logging.Abstractions;
using static EnvelopeGenerator.Jobs.FinalizeDocument.FinalizeDocumentExceptions;
using LayoutDocument = iText.Layout.Document;
namespace EnvelopeGenerator.Jobs.FinalizeDocument;
public class ReportCreator
{
private readonly ILogger<ReportCreator> _logger;
public ReportCreator() : this(NullLogger<ReportCreator>.Instance)
{
}
public ReportCreator(ILogger<ReportCreator> logger)
{
_logger = logger;
}
public byte[] CreateReport(SqlConnection connection, Envelope envelope)
{
try
{
var reportItems = LoadReportItems(connection, envelope.Id);
using var stream = new MemoryStream();
using var writer = new PdfWriter(stream);
using var pdf = new PdfDocument(writer);
using var document = new LayoutDocument(pdf);
document.Add(new Paragraph("Envelope Finalization Report").SetFontSize(16));
document.Add(new Paragraph($"Envelope Id: {envelope.Id}"));
document.Add(new Paragraph($"UUID: {envelope.Uuid}"));
document.Add(new Paragraph($"Title: {envelope.Title}"));
document.Add(new Paragraph($"Subject: {envelope.Comment}"));
document.Add(new Paragraph($"Generated: {DateTime.UtcNow:O}"));
document.Add(new Paragraph(" "));
var table = new Table(4).UseAllAvailableWidth();
table.AddHeaderCell("Date");
table.AddHeaderCell("Status");
table.AddHeaderCell("User");
table.AddHeaderCell("EnvelopeId");
foreach (var item in reportItems.OrderByDescending(r => r.ItemDate))
{
table.AddCell(item.ItemDate.ToString("u"));
table.AddCell(item.ItemStatus.ToString());
table.AddCell(item.ItemUserReference);
table.AddCell(item.EnvelopeId.ToString());
}
document.Add(table);
document.Close();
return stream.ToArray();
}
catch (Exception ex)
{
_logger.LogError(ex, "Could not create report for envelope {EnvelopeId}", envelope.Id);
throw new CreateReportException("Could not prepare report data", ex);
}
}
private List<ReportItem> LoadReportItems(SqlConnection connection, int envelopeId)
{
const string sql = "SELECT ENVELOPE_ID, POS_WHEN, POS_STATUS, POS_WHO FROM VWSIG_ENVELOPE_REPORT WHERE ENVELOPE_ID = @EnvelopeId";
var result = new List<ReportItem>();
using var command = new SqlCommand(sql, connection);
command.Parameters.AddWithValue("@EnvelopeId", envelopeId);
using var reader = command.ExecuteReader();
while (reader.Read())
{
result.Add(new ReportItem
{
EnvelopeId = reader.GetInt32(0),
ItemDate = reader.IsDBNull(1) ? DateTime.MinValue : reader.GetDateTime(1),
ItemStatus = reader.IsDBNull(2) ? default : (EnvelopeGenerator.Domain.Constants.EnvelopeStatus)reader.GetInt32(2),
ItemUserReference = reader.IsDBNull(3) ? string.Empty : reader.GetString(3)
});
}
return result;
}
}

View File

@@ -1,19 +0,0 @@
using EnvelopeGenerator.Domain.Constants;
using EnvelopeGenerator.Domain.Entities;
namespace EnvelopeGenerator.Jobs.FinalizeDocument;
public class ReportItem
{
public Envelope? Envelope { get; set; }
public int EnvelopeId { get; set; }
public string EnvelopeTitle { get; set; } = string.Empty;
public string EnvelopeSubject { get; set; } = string.Empty;
public EnvelopeStatus ItemStatus { get; set; }
public string ItemStatusTranslated => ItemStatus.ToString();
public string ItemUserReference { get; set; } = string.Empty;
public DateTime ItemDate { get; set; }
}

View File

@@ -1,8 +0,0 @@
using System.Collections.Generic;
namespace EnvelopeGenerator.Jobs.FinalizeDocument;
public class ReportSource
{
public List<ReportItem> Items { get; set; } = new();
}

View File

@@ -0,0 +1,20 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<base href="/" />
<link rel="stylesheet" href="bootstrap/bootstrap.min.css" />
<link rel="stylesheet" href="app.css" />
<link rel="stylesheet" href="EnvelopeGenerator.ReceiverUI.styles.css" />
<link rel="icon" type="image/png" href="favicon.png" />
<HeadOutlet />
</head>
<body>
<Routes />
<script src="_framework/blazor.web.js"></script>
</body>
</html>

View File

@@ -0,0 +1,23 @@
@inherits LayoutComponentBase
<div class="page">
<div class="sidebar">
<NavMenu />
</div>
<main>
<div class="top-row px-4">
<a href="https://learn.microsoft.com/aspnet/core/" target="_blank">About</a>
</div>
<article class="content px-4">
@Body
</article>
</main>
</div>
<div id="blazor-error-ui">
An unhandled error has occurred.
<a href="" class="reload">Reload</a>
<a class="dismiss">🗙</a>
</div>

View File

@@ -0,0 +1,96 @@
.page {
position: relative;
display: flex;
flex-direction: column;
}
main {
flex: 1;
}
.sidebar {
background-image: linear-gradient(180deg, rgb(5, 39, 103) 0%, #3a0647 70%);
}
.top-row {
background-color: #f7f7f7;
border-bottom: 1px solid #d6d5d5;
justify-content: flex-end;
height: 3.5rem;
display: flex;
align-items: center;
}
.top-row ::deep a, .top-row ::deep .btn-link {
white-space: nowrap;
margin-left: 1.5rem;
text-decoration: none;
}
.top-row ::deep a:hover, .top-row ::deep .btn-link:hover {
text-decoration: underline;
}
.top-row ::deep a:first-child {
overflow: hidden;
text-overflow: ellipsis;
}
@media (max-width: 640.98px) {
.top-row {
justify-content: space-between;
}
.top-row ::deep a, .top-row ::deep .btn-link {
margin-left: 0;
}
}
@media (min-width: 641px) {
.page {
flex-direction: row;
}
.sidebar {
width: 250px;
height: 100vh;
position: sticky;
top: 0;
}
.top-row {
position: sticky;
top: 0;
z-index: 1;
}
.top-row.auth ::deep a:first-child {
flex: 1;
text-align: right;
width: 0;
}
.top-row, article {
padding-left: 2rem !important;
padding-right: 1.5rem !important;
}
}
#blazor-error-ui {
background: lightyellow;
bottom: 0;
box-shadow: 0 -1px 2px rgba(0, 0, 0, 0.2);
display: none;
left: 0;
padding: 0.6rem 1.25rem 0.7rem 1.25rem;
position: fixed;
width: 100%;
z-index: 1000;
}
#blazor-error-ui .dismiss {
cursor: pointer;
position: absolute;
right: 0.75rem;
top: 0.5rem;
}

View File

@@ -0,0 +1,30 @@
<div class="top-row ps-3 navbar navbar-dark">
<div class="container-fluid">
<a class="navbar-brand" href="">EnvelopeGenerator.ReceiverUI</a>
</div>
</div>
<input type="checkbox" title="Navigation menu" class="navbar-toggler" />
<div class="nav-scrollable" onclick="document.querySelector('.navbar-toggler').click()">
<nav class="flex-column">
<div class="nav-item px-3">
<NavLink class="nav-link" href="" Match="NavLinkMatch.All">
<span class="bi bi-house-door-fill-nav-menu" aria-hidden="true"></span> Home
</NavLink>
</div>
<div class="nav-item px-3">
<NavLink class="nav-link" href="counter">
<span class="bi bi-plus-square-fill-nav-menu" aria-hidden="true"></span> Counter
</NavLink>
</div>
<div class="nav-item px-3">
<NavLink class="nav-link" href="weather">
<span class="bi bi-list-nested-nav-menu" aria-hidden="true"></span> Weather
</NavLink>
</div>
</nav>
</div>

View File

@@ -0,0 +1,105 @@
.navbar-toggler {
appearance: none;
cursor: pointer;
width: 3.5rem;
height: 2.5rem;
color: white;
position: absolute;
top: 0.5rem;
right: 1rem;
border: 1px solid rgba(255, 255, 255, 0.1);
background: url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 30 30'%3e%3cpath stroke='rgba%28255, 255, 255, 0.55%29' stroke-linecap='round' stroke-miterlimit='10' stroke-width='2' d='M4 7h22M4 15h22M4 23h22'/%3e%3c/svg%3e") no-repeat center/1.75rem rgba(255, 255, 255, 0.1);
}
.navbar-toggler:checked {
background-color: rgba(255, 255, 255, 0.5);
}
.top-row {
height: 3.5rem;
background-color: rgba(0,0,0,0.4);
}
.navbar-brand {
font-size: 1.1rem;
}
.bi {
display: inline-block;
position: relative;
width: 1.25rem;
height: 1.25rem;
margin-right: 0.75rem;
top: -1px;
background-size: cover;
}
.bi-house-door-fill-nav-menu {
background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='16' height='16' fill='white' class='bi bi-house-door-fill' viewBox='0 0 16 16'%3E%3Cpath d='M6.5 14.5v-3.505c0-.245.25-.495.5-.495h2c.25 0 .5.25.5.5v3.5a.5.5 0 0 0 .5.5h4a.5.5 0 0 0 .5-.5v-7a.5.5 0 0 0-.146-.354L13 5.793V2.5a.5.5 0 0 0-.5-.5h-1a.5.5 0 0 0-.5.5v1.293L8.354 1.146a.5.5 0 0 0-.708 0l-6 6A.5.5 0 0 0 1.5 7.5v7a.5.5 0 0 0 .5.5h4a.5.5 0 0 0 .5-.5Z'/%3E%3C/svg%3E");
}
.bi-plus-square-fill-nav-menu {
background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='16' height='16' fill='white' class='bi bi-plus-square-fill' viewBox='0 0 16 16'%3E%3Cpath d='M2 0a2 2 0 0 0-2 2v12a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V2a2 2 0 0 0-2-2H2zm6.5 4.5v3h3a.5.5 0 0 1 0 1h-3v3a.5.5 0 0 1-1 0v-3h-3a.5.5 0 0 1 0-1h3v-3a.5.5 0 0 1 1 0z'/%3E%3C/svg%3E");
}
.bi-list-nested-nav-menu {
background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='16' height='16' fill='white' class='bi bi-list-nested' viewBox='0 0 16 16'%3E%3Cpath fill-rule='evenodd' d='M4.5 11.5A.5.5 0 0 1 5 11h10a.5.5 0 0 1 0 1H5a.5.5 0 0 1-.5-.5zm-2-4A.5.5 0 0 1 3 7h10a.5.5 0 0 1 0 1H3a.5.5 0 0 1-.5-.5zm-2-4A.5.5 0 0 1 1 3h10a.5.5 0 0 1 0 1H1a.5.5 0 0 1-.5-.5z'/%3E%3C/svg%3E");
}
.nav-item {
font-size: 0.9rem;
padding-bottom: 0.5rem;
}
.nav-item:first-of-type {
padding-top: 1rem;
}
.nav-item:last-of-type {
padding-bottom: 1rem;
}
.nav-item ::deep .nav-link {
color: #d7d7d7;
background: none;
border: none;
border-radius: 4px;
height: 3rem;
display: flex;
align-items: center;
line-height: 3rem;
width: 100%;
}
.nav-item ::deep a.active {
background-color: rgba(255,255,255,0.37);
color: white;
}
.nav-item ::deep .nav-link:hover {
background-color: rgba(255,255,255,0.1);
color: white;
}
.nav-scrollable {
display: none;
}
.navbar-toggler:checked ~ .nav-scrollable {
display: block;
}
@media (min-width: 641px) {
.navbar-toggler {
display: none;
}
.nav-scrollable {
/* Never collapse the sidebar for wide screens */
display: block;
/* Allow sidebar to scroll for tall menus */
height: calc(100vh - 3.5rem);
overflow-y: auto;
}
}

View File

@@ -0,0 +1,19 @@
@page "/counter"
@rendermode InteractiveServer
<PageTitle>Counter</PageTitle>
<h1>Counter</h1>
<p role="status">Current count: @currentCount</p>
<button class="btn btn-primary" @onclick="IncrementCount">Click me</button>
@code {
private int currentCount = 0;
private void IncrementCount()
{
currentCount++;
}
}

View File

@@ -0,0 +1,36 @@
@page "/Error"
@using System.Diagnostics
<PageTitle>Error</PageTitle>
<h1 class="text-danger">Error.</h1>
<h2 class="text-danger">An error occurred while processing your request.</h2>
@if (ShowRequestId)
{
<p>
<strong>Request ID:</strong> <code>@RequestId</code>
</p>
}
<h3>Development Mode</h3>
<p>
Swapping to <strong>Development</strong> environment will display more detailed information about the error that occurred.
</p>
<p>
<strong>The Development environment shouldn't be enabled for deployed applications.</strong>
It can result in displaying sensitive information from exceptions to end users.
For local debugging, enable the <strong>Development</strong> environment by setting the <strong>ASPNETCORE_ENVIRONMENT</strong> environment variable to <strong>Development</strong>
and restarting the app.
</p>
@code{
[CascadingParameter]
private HttpContext? HttpContext { get; set; }
private string? RequestId { get; set; }
private bool ShowRequestId => !string.IsNullOrEmpty(RequestId);
protected override void OnInitialized() =>
RequestId = Activity.Current?.Id ?? HttpContext?.TraceIdentifier;
}

View File

@@ -0,0 +1,7 @@
@page "/"
<PageTitle>Home</PageTitle>
<h1>Hello, world!</h1>
Welcome to your new app.

View File

@@ -0,0 +1,64 @@
@page "/weather"
@attribute [StreamRendering]
<PageTitle>Weather</PageTitle>
<h1>Weather</h1>
<p>This component demonstrates showing data.</p>
@if (forecasts == null)
{
<p><em>Loading...</em></p>
}
else
{
<table class="table">
<thead>
<tr>
<th>Date</th>
<th>Temp. (C)</th>
<th>Temp. (F)</th>
<th>Summary</th>
</tr>
</thead>
<tbody>
@foreach (var forecast in forecasts)
{
<tr>
<td>@forecast.Date.ToShortDateString()</td>
<td>@forecast.TemperatureC</td>
<td>@forecast.TemperatureF</td>
<td>@forecast.Summary</td>
</tr>
}
</tbody>
</table>
}
@code {
private WeatherForecast[]? forecasts;
protected override async Task OnInitializedAsync()
{
// Simulate asynchronous loading to demonstrate streaming rendering
await Task.Delay(500);
var startDate = DateOnly.FromDateTime(DateTime.Now);
var summaries = new[] { "Freezing", "Bracing", "Chilly", "Cool", "Mild", "Warm", "Balmy", "Hot", "Sweltering", "Scorching" };
forecasts = Enumerable.Range(1, 5).Select(index => new WeatherForecast
{
Date = startDate.AddDays(index),
TemperatureC = Random.Shared.Next(-20, 55),
Summary = summaries[Random.Shared.Next(summaries.Length)]
}).ToArray();
}
private class WeatherForecast
{
public DateOnly Date { get; set; }
public int TemperatureC { get; set; }
public string? Summary { get; set; }
public int TemperatureF => 32 + (int)(TemperatureC / 0.5556);
}
}

View File

@@ -0,0 +1,6 @@
<Router AppAssembly="typeof(Program).Assembly">
<Found Context="routeData">
<RouteView RouteData="routeData" DefaultLayout="typeof(Layout.MainLayout)" />
<FocusOnNavigate RouteData="routeData" Selector="h1" />
</Found>
</Router>

View File

@@ -0,0 +1,10 @@
@using System.Net.Http
@using System.Net.Http.Json
@using Microsoft.AspNetCore.Components.Forms
@using Microsoft.AspNetCore.Components.Routing
@using Microsoft.AspNetCore.Components.Web
@using static Microsoft.AspNetCore.Components.Web.RenderMode
@using Microsoft.AspNetCore.Components.Web.Virtualization
@using Microsoft.JSInterop
@using EnvelopeGenerator.ReceiverUI
@using EnvelopeGenerator.ReceiverUI.Components

View File

@@ -0,0 +1,9 @@
<Project Sdk="Microsoft.NET.Sdk.Web">
<PropertyGroup>
<TargetFramework>net8.0</TargetFramework>
<Nullable>enable</Nullable>
<ImplicitUsings>enable</ImplicitUsings>
</PropertyGroup>
</Project>

View File

@@ -0,0 +1,27 @@
using EnvelopeGenerator.ReceiverUI.Components;
var builder = WebApplication.CreateBuilder(args);
// Add services to the container.
builder.Services.AddRazorComponents()
.AddInteractiveServerComponents();
var app = builder.Build();
// Configure the HTTP request pipeline.
if (!app.Environment.IsDevelopment())
{
app.UseExceptionHandler("/Error", createScopeForErrors: true);
// The default HSTS value is 30 days. You may want to change this for production scenarios, see https://aka.ms/aspnetcore-hsts.
app.UseHsts();
}
app.UseHttpsRedirection();
app.UseStaticFiles();
app.UseAntiforgery();
app.MapRazorComponents<App>()
.AddInteractiveServerRenderMode();
app.Run();

View File

@@ -0,0 +1,38 @@
{
"$schema": "http://json.schemastore.org/launchsettings.json",
"iisSettings": {
"windowsAuthentication": false,
"anonymousAuthentication": true,
"iisExpress": {
"applicationUrl": "http://localhost:26087",
"sslPort": 44331
}
},
"profiles": {
"http": {
"commandName": "Project",
"dotnetRunMessages": true,
"launchBrowser": true,
"applicationUrl": "http://localhost:5134",
"environmentVariables": {
"ASPNETCORE_ENVIRONMENT": "Development"
}
},
"https": {
"commandName": "Project",
"dotnetRunMessages": true,
"launchBrowser": true,
"applicationUrl": "https://localhost:7124;http://localhost:5134",
"environmentVariables": {
"ASPNETCORE_ENVIRONMENT": "Development"
}
},
"IIS Express": {
"commandName": "IISExpress",
"launchBrowser": true,
"environmentVariables": {
"ASPNETCORE_ENVIRONMENT": "Development"
}
}
}
}

View File

@@ -0,0 +1,8 @@
{
"Logging": {
"LogLevel": {
"Default": "Information",
"Microsoft.AspNetCore": "Warning"
}
}
}

View File

@@ -0,0 +1,9 @@
{
"Logging": {
"LogLevel": {
"Default": "Information",
"Microsoft.AspNetCore": "Warning"
}
},
"AllowedHosts": "*"
}

View File

@@ -0,0 +1,51 @@
html, body {
font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif;
}
a, .btn-link {
color: #006bb7;
}
.btn-primary {
color: #fff;
background-color: #1b6ec2;
border-color: #1861ac;
}
.btn:focus, .btn:active:focus, .btn-link.nav-link:focus, .form-control:focus, .form-check-input:focus {
box-shadow: 0 0 0 0.1rem white, 0 0 0 0.25rem #258cfb;
}
.content {
padding-top: 1.1rem;
}
h1:focus {
outline: none;
}
.valid.modified:not([type=checkbox]) {
outline: 1px solid #26b050;
}
.invalid {
outline: 1px solid #e50000;
}
.validation-message {
color: #e50000;
}
.blazor-error-boundary {
background: url(data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iNTYiIGhlaWdodD0iNDkiIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyIgeG1sbnM6eGxpbms9Imh0dHA6Ly93d3cudzMub3JnLzE5OTkveGxpbmsiIG92ZXJmbG93PSJoaWRkZW4iPjxkZWZzPjxjbGlwUGF0aCBpZD0iY2xpcDAiPjxyZWN0IHg9IjIzNSIgeT0iNTEiIHdpZHRoPSI1NiIgaGVpZ2h0PSI0OSIvPjwvY2xpcFBhdGg+PC9kZWZzPjxnIGNsaXAtcGF0aD0idXJsKCNjbGlwMCkiIHRyYW5zZm9ybT0idHJhbnNsYXRlKC0yMzUgLTUxKSI+PHBhdGggZD0iTTI2My41MDYgNTFDMjY0LjcxNyA1MSAyNjUuODEzIDUxLjQ4MzcgMjY2LjYwNiA1Mi4yNjU4TDI2Ny4wNTIgNTIuNzk4NyAyNjcuNTM5IDUzLjYyODMgMjkwLjE4NSA5Mi4xODMxIDI5MC41NDUgOTIuNzk1IDI5MC42NTYgOTIuOTk2QzI5MC44NzcgOTMuNTEzIDI5MSA5NC4wODE1IDI5MSA5NC42NzgyIDI5MSA5Ny4wNjUxIDI4OS4wMzggOTkgMjg2LjYxNyA5OUwyNDAuMzgzIDk5QzIzNy45NjMgOTkgMjM2IDk3LjA2NTEgMjM2IDk0LjY3ODIgMjM2IDk0LjM3OTkgMjM2LjAzMSA5NC4wODg2IDIzNi4wODkgOTMuODA3MkwyMzYuMzM4IDkzLjAxNjIgMjM2Ljg1OCA5Mi4xMzE0IDI1OS40NzMgNTMuNjI5NCAyNTkuOTYxIDUyLjc5ODUgMjYwLjQwNyA1Mi4yNjU4QzI2MS4yIDUxLjQ4MzcgMjYyLjI5NiA1MSAyNjMuNTA2IDUxWk0yNjMuNTg2IDY2LjAxODNDMjYwLjczNyA2Ni4wMTgzIDI1OS4zMTMgNjcuMTI0NSAyNTkuMzEzIDY5LjMzNyAyNTkuMzEzIDY5LjYxMDIgMjU5LjMzMiA2OS44NjA4IDI1OS4zNzEgNzAuMDg4N0wyNjEuNzk1IDg0LjAxNjEgMjY1LjM4IDg0LjAxNjEgMjY3LjgyMSA2OS43NDc1QzI2Ny44NiA2OS43MzA5IDI2Ny44NzkgNjkuNTg3NyAyNjcuODc5IDY5LjMxNzkgMjY3Ljg3OSA2Ny4xMTgyIDI2Ni40NDggNjYuMDE4MyAyNjMuNTg2IDY2LjAxODNaTTI2My41NzYgODYuMDU0N0MyNjEuMDQ5IDg2LjA1NDcgMjU5Ljc4NiA4Ny4zMDA1IDI1OS43ODYgODkuNzkyMSAyNTkuNzg2IDkyLjI4MzcgMjYxLjA0OSA5My41Mjk1IDI2My41NzYgOTMuNTI5NSAyNjYuMTE2IDkzLjUyOTUgMjY3LjM4NyA5Mi4yODM3IDI2Ny4zODcgODkuNzkyMSAyNjcuMzg3IDg3LjMwMDUgMjY2LjExNiA4Ni4wNTQ3IDI2My41NzYgODYuMDU0N1oiIGZpbGw9IiNGRkU1MDAiIGZpbGwtcnVsZT0iZXZlbm9kZCIvPjwvZz48L3N2Zz4=) no-repeat 1rem/1.8rem, #b32121;
padding: 1rem 1rem 1rem 3.7rem;
color: white;
}
.blazor-error-boundary::after {
content: "An error has occurred."
}
.darker-border-checkbox.form-check-input {
border-color: #929292;
}

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 KiB

View File

@@ -0,0 +1,13 @@
@using Microsoft.AspNetCore.Components.Routing
<Router AppAssembly="@typeof(App).Assembly">
<Found Context="routeData">
<RouteView RouteData="routeData" DefaultLayout="typeof(MainLayout)" />
<FocusOnNavigate RouteData="routeData" Selector="h1" />
</Found>
<NotFound>
<LayoutView Layout="typeof(MainLayout)">
<p role="alert">Sorry, there's nothing at this address.</p>
</LayoutView>
</NotFound>
</Router>

View File

@@ -0,0 +1,13 @@
<Project Sdk="Microsoft.NET.Sdk.BlazorWebAssembly">
<PropertyGroup>
<TargetFramework>net8.0</TargetFramework>
<Nullable>enable</Nullable>
<ImplicitUsings>enable</ImplicitUsings>
<AssemblyName>EnvelopeGenerator.ReceiverUIBlazor</AssemblyName>
<RootNamespace>EnvelopeGenerator.ReceiverUIBlazor</RootNamespace>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Microsoft.AspNetCore.Components.WebAssembly" Version="8.0.0" />
<PackageReference Include="Microsoft.AspNetCore.Components.WebAssembly.DevServer" Version="8.0.0" PrivateAssets="all" />
</ItemGroup>
</Project>

View File

@@ -0,0 +1,428 @@
@page "/"
@using Microsoft.JSInterop
@inject IJSRuntime JS
@implements IAsyncDisposable
<div class="page-shell">
<h1>Sign PDF (Blazor)</h1>
<div class="controls">
<InputFile OnChange="HandleFileSelected" accept="application/pdf" />
<button class="btn" @onclick="ShowSignaturePad" disabled="@(!HasPdf)">Add signature</button>
<button class="btn" @onclick="() => ShowTextOverlay(false)" disabled="@(!HasPdf)">Add text</button>
<button class="btn" @onclick="() => ShowTextOverlay(true)" disabled="@(!HasPdf)">Add date</button>
<button class="btn" @onclick="Reset" disabled="@(!HasPdf)">Reset</button>
<button class="btn secondary" @onclick="Download" disabled="@(!HasPdf)">Download</button>
</div>
@if (!string.IsNullOrWhiteSpace(ErrorMessage))
{
<div class="error-banner">@ErrorMessage</div>
}
@if (!HasPdf)
{
<div class="drop-hint">Drop or select a PDF to start.</div>
}
@if (HasPdf)
{
<div class="document-shell" @ref="PdfHostRef" style="@($"width:{ViewportWidthPx}px")">
<canvas id="pdf-canvas" @ref="PdfCanvasRef"></canvas>
@if (ShowSignature)
{
<div class="overlay signature" @ref="OverlayRef"
style="@($"left:{OverlayXpx}px; top:{OverlayYpx}px; width:{OverlayWidthPx}px; height:{OverlayHeightPx}px;")"
@onpointerdown="StartDrag" @onpointermove="OnDrag" @onpointerup="EndDrag" @onpointercancel="EndDrag">
<div class="overlay-controls">
<button class="overlay-btn" @onclick="ApplySignature" @onpointerdown:stopPropagation @onpointerup:stopPropagation @onclick:stopPropagation>✔</button>
<button class="overlay-btn" @onclick="CancelOverlay" @onpointerdown:stopPropagation @onpointerup:stopPropagation @onclick:stopPropagation>✖</button>
</div>
<img src="@SignatureDataUrl" draggable="false" />
</div>
}
@if (ShowText)
{
<div class="overlay text" @ref="OverlayRef"
style="@($"left:{OverlayXpx}px; top:{OverlayYpx}px;")"
@onpointerdown="StartDrag" @onpointermove="OnDrag" @onpointerup="EndDrag" @onpointercancel="EndDrag">
<div class="overlay-controls">
<button class="overlay-btn" @onclick="ApplyText" @onpointerdown:stopPropagation @onpointerup:stopPropagation @onclick:stopPropagation>✔</button>
<button class="overlay-btn" @onclick="CancelOverlay" @onpointerdown:stopPropagation @onpointerup:stopPropagation @onclick:stopPropagation>✖</button>
</div>
<input class="overlay-input" @bind="TextValue" @onpointerdown:stopPropagation @onpointerup:stopPropagation @onclick:stopPropagation />
</div>
}
</div>
<div class="paging">
<button class="btn" @onclick="PrevPage" disabled="@(!CanPrev)">&lt;</button>
<span>Page @DisplayPage / @PageCount</span>
<button class="btn" @onclick="NextPage" disabled="@(!CanNext)">&gt;</button>
</div>
}
</div>
@if (ShowSignaturePadModal)
{
<div class="modal-backdrop">
<div class="modal">
<h3>Add signature</h3>
<canvas id="@SignatureCanvasId" width="700" height="220"></canvas>
<div class="modal-row">
<label><input type="checkbox" @bind="AutoDate" /> Auto date/time</label>
<button class="btn" @onclick="ClearSignature">Clear</button>
<span class="spacer"></span>
<button class="btn" @onclick="ConfirmSignature">Use signature</button>
<button class="btn secondary" @onclick="CloseSignaturePad">Cancel</button>
</div>
</div>
</div>
}
@code {
private ElementReference PdfCanvasRef;
private ElementReference PdfHostRef;
private ElementReference OverlayRef;
private string? PdfBase64;
private string? OriginalPdfBase64;
private DotNetObjectReference<Index>? _dotNetRef;
private int PageIndex;
private int PageCount;
private double ViewportWidthPx = 800;
private double ViewportHeightPx;
private bool ShowSignaturePadModal;
private bool ShowSignature;
private bool ShowText;
private string SignatureCanvasId { get; } = $"sig-{Guid.NewGuid():N}";
private string? SignatureDataUrl;
private bool AutoDate = true;
private double OverlayXpx = 20;
private double OverlayYpx = 20;
private double OverlayWidthPx = 200;
private double OverlayHeightPx = 80;
private bool IsDragging;
private double DragStartX;
private double DragStartY;
private double StartLeft;
private double StartTop;
private string TextValue = "Text";
private string? ErrorMessage;
private DateTimeOffset _lastDragRender = DateTimeOffset.MinValue;
private bool HasPdf => !string.IsNullOrWhiteSpace(PdfBase64);
private int DisplayPage => PageIndex + 1;
private bool CanPrev => PageIndex > 0;
private bool CanNext => PageIndex + 1 < PageCount;
protected override async Task OnAfterRenderAsync(bool firstRender)
{
if (firstRender)
{
await JS.InvokeVoidAsync("pdfInterop.ensureReady");
_dotNetRef ??= DotNetObjectReference.Create(this);
await JS.InvokeVoidAsync("pdfInterop.registerDropHandler", _dotNetRef);
}
if (ShowSignaturePadModal)
{
await JS.InvokeVoidAsync("pdfInterop.initSignaturePad", SignatureCanvasId);
}
}
private async Task HandleFileSelected(InputFileChangeEventArgs e)
{
if (e.FileCount == 0)
{
return;
}
await LoadPdfFromBrowserFile(e.File);
}
private async Task LoadPdfFromBrowserFile(IBrowserFile file)
{
ErrorMessage = null;
try
{
await using var stream = file.OpenReadStream(maxAllowedSize: 20 * 1024 * 1024);
using var ms = new MemoryStream();
await stream.CopyToAsync(ms);
await LoadPdfFromBase64Internal(Convert.ToBase64String(ms.ToArray()));
}
catch (Exception ex)
{
ErrorMessage = $"Fehler beim Laden der PDF: {ex.Message}";
PdfBase64 = null;
PageCount = 0;
PageIndex = 0;
}
}
[JSInvokable]
public Task LoadPdfFromBase64(string base64)
{
return LoadPdfFromBase64Internal(base64);
}
private async Task LoadPdfFromBase64Internal(string base64)
{
ErrorMessage = null;
PdfBase64 = base64;
OriginalPdfBase64 = PdfBase64;
// Show the canvas before we start rendering
await InvokeAsync(StateHasChanged);
await Task.Yield();
// Make sure pdf.js is ready
await JS.InvokeVoidAsync("pdfInterop.ensureReady");
var result = await JS.InvokeAsync<RenderResult>("pdfInterop.loadPdf", PdfBase64);
PageCount = result.Pages;
PageIndex = 0;
await RenderPage();
}
private async Task RenderPage()
{
if (!HasPdf)
{
return;
}
try
{
var viewport = await JS.InvokeAsync<ViewportInfo>("pdfInterop.renderPage", PageIndex, "pdf-canvas", ViewportWidthPx);
ViewportWidthPx = viewport.Width;
ViewportHeightPx = viewport.Height;
StateHasChanged();
}
catch (Exception ex)
{
ErrorMessage = $"Fehler beim Rendern: {ex.Message}";
}
}
private async Task Reset()
{
ErrorMessage = null;
CloseOverlays();
ShowSignaturePadModal = false;
OverlayXpx = 20;
OverlayYpx = 20;
OverlayWidthPx = 200;
OverlayHeightPx = 80;
TextValue = "Text";
if (string.IsNullOrWhiteSpace(OriginalPdfBase64))
{
return;
}
PdfBase64 = OriginalPdfBase64;
PageIndex = 0;
var result = await JS.InvokeAsync<RenderResult>("pdfInterop.loadPdf", PdfBase64);
PageCount = result.Pages;
await RenderPage();
}
private void CloseOverlays()
{
ShowSignature = false;
ShowText = false;
SignatureDataUrl = null;
}
private void ShowSignaturePad()
{
ShowSignaturePadModal = true;
}
private async Task ConfirmSignature()
{
SignatureDataUrl = await JS.InvokeAsync<string>("pdfInterop.getSignatureDataUrl", SignatureCanvasId);
if (string.IsNullOrWhiteSpace(SignatureDataUrl))
{
return;
}
OverlayWidthPx = 200;
OverlayHeightPx = 80;
OverlayXpx = 20;
OverlayYpx = 20;
ShowSignature = true;
ShowText = false;
ShowSignaturePadModal = false;
}
private void CloseSignaturePad()
{
ShowSignaturePadModal = false;
}
private void ClearSignature()
{
JS.InvokeVoidAsync("pdfInterop.clearSignaturePad", SignatureCanvasId);
}
private void ShowTextOverlay(bool autoDate)
{
TextValue = autoDate ? DateTimeOffset.Now.ToString("M/d/yyyy HH:mm:ss zzz") : "Text";
OverlayWidthPx = 240;
OverlayHeightPx = 40;
OverlayXpx = 20;
OverlayYpx = 20;
ShowText = true;
ShowSignature = false;
}
private void StartDrag(PointerEventArgs args)
{
IsDragging = true;
DragStartX = args.ClientX;
DragStartY = args.ClientY;
StartLeft = OverlayXpx;
StartTop = OverlayYpx;
if (OverlayRef.Context != null)
{
JS.InvokeVoidAsync("pdfInterop.capturePointer", OverlayRef, args.PointerId);
}
}
private void OnDrag(PointerEventArgs args)
{
if (!IsDragging)
{
return;
}
var dx = args.ClientX - DragStartX;
var dy = args.ClientY - DragStartY;
OverlayXpx = StartLeft + dx;
OverlayYpx = StartTop + dy;
var now = DateTimeOffset.UtcNow;
if (now - _lastDragRender > TimeSpan.FromMilliseconds(16))
{
_lastDragRender = now;
InvokeAsync(StateHasChanged);
}
}
private void EndDrag(PointerEventArgs args)
{
IsDragging = false;
if (OverlayRef.Context != null)
{
JS.InvokeVoidAsync("pdfInterop.releasePointer", OverlayRef, args.PointerId);
}
}
private async Task ApplySignature()
{
if (SignatureDataUrl is null || !HasPdf)
{
return;
}
PdfBase64 = await JS.InvokeAsync<string>("pdfInterop.applySignature", new
{
base64 = PdfBase64,
pageIndex = PageIndex,
left = OverlayXpx,
top = OverlayYpx,
width = OverlayWidthPx,
height = OverlayHeightPx,
renderWidth = ViewportWidthPx,
renderHeight = ViewportHeightPx,
dataUrl = SignatureDataUrl,
autoDate = AutoDate,
});
CloseOverlays();
await RenderPage();
}
private async Task ApplyText()
{
if (string.IsNullOrWhiteSpace(TextValue) || !HasPdf)
{
return;
}
PdfBase64 = await JS.InvokeAsync<string>("pdfInterop.applyText", new
{
base64 = PdfBase64,
pageIndex = PageIndex,
left = OverlayXpx,
top = OverlayYpx,
width = OverlayWidthPx,
height = OverlayHeightPx,
renderWidth = ViewportWidthPx,
renderHeight = ViewportHeightPx,
text = TextValue,
fontSize = 20,
});
CloseOverlays();
await RenderPage();
}
private void CancelOverlay()
{
CloseOverlays();
}
private async Task PrevPage()
{
if (!CanPrev)
{
return;
}
PageIndex--;
await RenderPage();
}
private async Task NextPage()
{
if (!CanNext)
{
return;
}
PageIndex++;
await RenderPage();
}
private async Task Download()
{
if (!HasPdf)
{
return;
}
await JS.InvokeVoidAsync("pdfInterop.downloadPdf", PdfBase64, "document-signed.pdf");
}
private record RenderResult(int Pages);
private record ViewportInfo(double Width, double Height, double PageWidth, double PageHeight);
public async ValueTask DisposeAsync()
{
_dotNetRef?.Dispose();
await Task.CompletedTask;
}
}

View File

@@ -0,0 +1,11 @@
using Microsoft.AspNetCore.Components.Web;
using Microsoft.AspNetCore.Components.WebAssembly.Hosting;
using EnvelopeGenerator.ReceiverUIBlazor;
var builder = WebAssemblyHostBuilder.CreateDefault(args);
builder.RootComponents.Add<App>("#app");
builder.RootComponents.Add<HeadOutlet>("head::after");
builder.Services.AddScoped(sp => new HttpClient { BaseAddress = new Uri(builder.HostEnvironment.BaseAddress) });
await builder.Build().RunAsync();

View File

@@ -0,0 +1,17 @@
<?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>7f262ad4-53b1-42d3-9a5f-132cf50f150c</ProjectGuid>
<DesktopBuildPackageLocation>E:\TekH\Visual Studio\src\EnvelopeGenerator.ReceiverUIBlazor\EnvelopeGenerator.ReceiverUIBlazor.zip</DesktopBuildPackageLocation>
<PackageAsSingleFile>true</PackageAsSingleFile>
<DeployIisAppPath>ReceiverUIBlazor</DeployIisAppPath>
<_TargetId>IISWebDeployPackage</_TargetId>
</PropertyGroup>
</Project>

View File

@@ -0,0 +1,12 @@
{
"profiles": {
"EnvelopeGenerator.ReceiverUIBlazor": {
"commandName": "Project",
"launchBrowser": true,
"environmentVariables": {
"ASPNETCORE_ENVIRONMENT": "Development"
},
"applicationUrl": "https://localhost:49582;http://localhost:49583"
}
}
}

View File

@@ -0,0 +1,10 @@
@inherits LayoutComponentBase
<div class="main-layout">
<header class="top-bar">
<div class="brand">Receiver UI (Blazor)</div>
</header>
<main class="content">
@Body
</main>
</div>

View File

@@ -0,0 +1,8 @@
@using System.Net.Http
@using Microsoft.AspNetCore.Components
@using Microsoft.AspNetCore.Components.Web
@using Microsoft.AspNetCore.Components.Forms
@using Microsoft.AspNetCore.Components.WebAssembly.Hosting
@using Microsoft.JSInterop
@using EnvelopeGenerator.ReceiverUIBlazor
@using EnvelopeGenerator.ReceiverUIBlazor.Shared

View File

@@ -0,0 +1,241 @@
@import url('https://fonts.googleapis.com/css2?family=Red+Hat+Text:wght@400;500;600;700&family=Teko:wght@500;600&display=swap');
:root {
--bg: #f7f7f8;
--bg-strong: #fff6f6;
--text: #474747;
--muted: #777777;
--border: #e7e7e7;
--shadow: 0 18px 55px rgba(20, 20, 20, 0.08);
--card: #ffffff;
--accent: #a52431;
--accent-strong: #8d1e2a;
--accent-soft: #f8e5e8;
--highlight: #ffd62f;
--danger: #a52431;
}
body {
margin: 0;
font-family: "Red Hat Text", "Segoe UI", system-ui, -apple-system, sans-serif;
background: radial-gradient(120% 120% at 6% 12%, var(--bg-strong) 0%, #fffdf7 45%, var(--bg) 85%);
color: var(--text);
line-height: 1.5;
}
.main-layout {
min-height: 100vh;
}
.top-bar {
display: flex;
align-items: center;
padding: 14px 24px;
background: var(--accent);
border-bottom: 1px solid var(--accent-strong);
box-shadow: 0 8px 24px rgba(0, 0, 0, 0.12);
color: #ffd62f;
}
.top-bar .brand {
font-family: "Teko", "Red Hat Text", sans-serif;
font-weight: 600;
letter-spacing: 0.6px;
color: #ffd62f;
}
.content {
padding: 28px 32px 40px;
max-width: 1200px;
margin: 0 auto;
display: flex;
flex-direction: column;
gap: 16px;
align-items: center;
}
h1 {
margin: 0 0 10px;
letter-spacing: 0.2px;
font-family: "Teko", "Red Hat Text", sans-serif;
font-weight: 600;
}
.controls {
display: flex;
gap: 10px;
flex-wrap: wrap;
align-items: center;
margin-bottom: 14px;
justify-content: center;
}
.btn {
background: #4a4a4a;
color: #ffffff;
border: 1px solid #404040;
padding: 10px 15px;
border-radius: 12px;
font-weight: 700;
cursor: pointer;
transition: transform 120ms ease, box-shadow 120ms ease, background-color 120ms ease;
box-shadow: 0 8px 16px rgba(0, 0, 0, 0.12);
}
.btn:hover:not(:disabled) {
transform: translateY(-1px);
background: #3f3f3f;
}
.btn:disabled {
opacity: 0.45;
cursor: not-allowed;
box-shadow: none;
}
.btn.secondary {
background: #f4f4f4;
color: #474747;
border: 1px solid #d3d3d3;
box-shadow: none;
}
.btn.secondary:hover:not(:disabled) {
background: #e9e9e9;
transform: translateY(-1px);
}
.drop-hint {
padding: 26px;
border: 1px dashed var(--border);
border-radius: 14px;
text-align: center;
color: var(--muted);
background: #ffffff;
width: min(1100px, 100%);
margin: 0 auto;
box-shadow: 0 8px 18px rgba(165, 36, 49, 0.06);
}
.error-banner {
margin-top: 10px;
padding: 12px 14px;
border-radius: 10px;
background: #fcebec;
border: 1px solid #f3c6cd;
color: var(--accent-strong);
}
.document-shell {
position: relative;
margin-top: 14px;
border-radius: 14px;
overflow: hidden;
box-shadow: var(--shadow);
background: var(--card);
border: 1px solid var(--border);
margin-left: auto;
margin-right: auto;
}
canvas {
display: block;
width: 100%;
height: auto;
background: #ffffff;
}
.overlay {
position: absolute;
border: 1px dashed var(--accent);
border-radius: 10px;
padding: 8px;
background: rgba(255, 255, 255, 0.96);
box-shadow: 0 12px 30px rgba(165, 36, 49, 0.15);
user-select: none;
touch-action: none;
}
.overlay.signature img {
display: block;
max-width: 100%;
max-height: 100%;
}
.overlay-controls {
position: absolute;
top: -44px;
right: 0;
display: flex;
gap: 6px;
}
.overlay-btn {
background: #ffffff;
color: var(--accent-strong);
border: 1px solid var(--accent);
border-radius: 8px;
padding: 6px 10px;
cursor: pointer;
box-shadow: 0 6px 16px rgba(165, 36, 49, 0.16);
}
.overlay-input {
border: 1px solid var(--border);
background: #ffffff;
color: var(--text);
font-size: 18px;
padding: 6px 8px;
min-width: 180px;
border-radius: 8px;
outline: none;
}
.paging {
margin-top: 14px;
display: flex;
gap: 10px;
align-items: center;
color: var(--muted);
}
.modal-backdrop {
position: fixed;
top: 0;
left: 0;
right: 0;
bottom: 0;
background: rgba(15, 23, 42, 0.35);
backdrop-filter: blur(3px);
display: flex;
align-items: center;
justify-content: center;
z-index: 2000;
}
.modal {
background: var(--card);
border: 1px solid var(--border);
border-radius: 14px;
padding: 18px;
min-width: 760px;
box-shadow: var(--shadow);
}
.modal-row {
display: flex;
align-items: center;
gap: 12px;
margin-top: 12px;
color: var(--muted);
}
.modal canvas {
background: #ffffff;
border: 1px solid var(--border);
border-radius: 10px;
}
.spacer {
flex: 1;
}

View File

@@ -0,0 +1,19 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Receiver UI (Blazor)</title>
<base href="/" />
<link rel="stylesheet" href="css/app.css" />
<!-- pdf.js 3.11 UMD + classic worker for compatibility; SRI removed to avoid digest mismatches -->
<script src="https://cdnjs.cloudflare.com/ajax/libs/pdf.js/3.11.174/pdf.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/pdf.js/3.11.174/pdf.worker.min.js"></script>
<script src="https://unpkg.com/pdf-lib/dist/pdf-lib.min.js"></script>
<script src="js/pdfInterop.js"></script>
</head>
<body>
<div id="app">Loading...</div>
<script src="_framework/blazor.webassembly.js"></script>
</body>
</html>

View File

@@ -0,0 +1,339 @@
(function () {
// Stick to pdf.js 3.11 UMD + classic worker for compatibility.
const PDF_JS_SRC = "https://cdnjs.cloudflare.com/ajax/libs/pdf.js/3.11.174/pdf.min.js";
const WORKER_SRC = "https://cdnjs.cloudflare.com/ajax/libs/pdf.js/3.11.174/pdf.worker.min.js";
const state = {
pdfDoc: null,
pdfBytes: null,
lastViewport: null,
pdfJsReady: null,
};
function base64ToUint8(base64) {
const binStr = atob(base64);
const len = binStr.length;
const bytes = new Uint8Array(len);
for (let i = 0; i < len; i++) {
bytes[i] = binStr.charCodeAt(i);
}
return bytes;
}
async function reloadFromBase64(base64) {
state.pdfBytes = base64ToUint8(base64);
state.pdfDoc = await pdfjsLib.getDocument({ data: state.pdfBytes }).promise;
return { pages: state.pdfDoc.numPages };
}
function dataUrlDownload(dataUrl, filename) {
const a = document.createElement('a');
a.href = dataUrl;
a.download = filename;
document.body.appendChild(a);
a.click();
document.body.removeChild(a);
}
const pointerPads = new Map();
function loadScriptOnce(url) {
return new Promise((resolve, reject) => {
// If already present, resolve immediately
const existing = Array.from(document.getElementsByTagName('script')).find(s => s.src === url);
if (existing && existing.dataset.loaded === "true") {
resolve();
return;
}
const script = existing || document.createElement('script');
script.src = url;
script.defer = true;
script.onload = () => {
script.dataset.loaded = "true";
resolve();
};
script.onerror = (e) => reject(new Error(`Script load failed: ${url}`));
if (!existing) {
document.head.appendChild(script);
}
});
}
async function ensurePdfJsLoaded() {
if (typeof pdfjsLib !== "undefined") {
return;
}
if (!state.pdfJsReady) {
state.pdfJsReady = loadScriptOnce(PDF_JS_SRC);
}
await state.pdfJsReady;
if (typeof pdfjsLib === "undefined") {
throw new Error("pdfjsLib could not be loaded");
}
}
window.pdfInterop = {
ensureReady: async () => {
// Ensure pdf.js is present and the worker path is set explicitly.
await ensurePdfJsLoaded();
if (pdfjsLib && pdfjsLib.GlobalWorkerOptions) {
if (pdfjsLib.GlobalWorkerOptions.workerSrc !== WORKER_SRC) {
pdfjsLib.GlobalWorkerOptions.workerSrc = WORKER_SRC;
}
} else {
throw new Error("pdf.js not available after load");
}
},
loadPdf: async (base64) => {
await ensurePdfJsLoaded();
try {
const result = await reloadFromBase64(base64);
if (!result || !result.pages) {
throw new Error("PDF has keine Seiten erkannt");
}
return result;
} catch (err) {
console.error("pdfInterop.loadPdf failed", err);
throw err;
}
},
renderPage: async (pageIndex, canvasId, targetWidth) => {
await ensurePdfJsLoaded();
if (!state.pdfDoc) {
throw new Error('PDF not loaded');
}
const page = await state.pdfDoc.getPage(pageIndex + 1);
const rawViewport = page.getViewport({ scale: 1 });
const scale = targetWidth / rawViewport.width;
const viewport = page.getViewport({ scale });
let canvas = document.getElementById(canvasId);
if (!canvas) {
// give the UI a tiny delay to render the canvas into the DOM
await new Promise(r => setTimeout(r, 40));
canvas = document.getElementById(canvasId);
}
if (!canvas) {
console.error("renderPage: canvas not found", canvasId);
throw new Error('Canvas not found');
}
const ctx = canvas.getContext('2d');
canvas.width = viewport.width;
canvas.height = viewport.height;
await page.render({ canvasContext: ctx, viewport }).promise;
state.lastViewport = {
width: viewport.width,
height: viewport.height,
pageWidth: rawViewport.width,
pageHeight: rawViewport.height,
};
return state.lastViewport;
},
applySignature: async (payload) => {
const {
base64,
pageIndex,
left,
top,
width,
height,
renderWidth,
renderHeight,
dataUrl,
autoDate,
} = payload;
const pdfDoc = await PDFLib.PDFDocument.load(base64ToUint8(base64));
const page = pdfDoc.getPage(pageIndex);
const scaleX = page.getWidth() / renderWidth;
const scaleY = page.getHeight() / renderHeight;
const pngImage = await pdfDoc.embedPng(dataUrl);
const drawWidth = width * scaleX;
const drawHeight = height * scaleY;
const x = left * scaleX;
const y = page.getHeight() - (top + height) * scaleY;
page.drawImage(pngImage, {
x,
y,
width: drawWidth,
height: drawHeight,
});
if (autoDate) {
const text = `Signed ${new Date().toLocaleString()}`;
page.drawText(text, {
x,
y: y - 14 * scaleY,
size: 14 * scaleX,
color: PDFLib.rgb(0.11, 0.25, 0.56),
});
}
const updatedBase64 = await pdfDoc.saveAsBase64({ dataUri: false });
await reloadFromBase64(updatedBase64);
return updatedBase64;
},
applyText: async (payload) => {
const {
base64,
pageIndex,
left,
top,
width,
height,
renderWidth,
renderHeight,
text,
fontSize,
} = payload;
const pdfDoc = await PDFLib.PDFDocument.load(base64ToUint8(base64));
const page = pdfDoc.getPage(pageIndex);
const scaleX = page.getWidth() / renderWidth;
const scaleY = page.getHeight() / renderHeight;
const x = left * scaleX;
const y = page.getHeight() - (top + height) * scaleY;
page.drawText(text, {
x,
y,
size: fontSize * scaleX,
color: PDFLib.rgb(0.2, 0.23, 0.28),
});
const updatedBase64 = await pdfDoc.saveAsBase64({ dataUri: false });
await reloadFromBase64(updatedBase64);
return updatedBase64;
},
downloadPdf: (base64, filename) => {
dataUrlDownload(`data:application/pdf;base64,${base64}`, filename);
},
initSignaturePad: (canvasId) => {
const canvas = document.getElementById(canvasId);
if (!canvas) return;
const ctx = canvas.getContext('2d');
ctx.lineWidth = 2;
ctx.lineCap = 'round';
ctx.strokeStyle = '#1c3d8f';
const padState = {
drawing: false,
lastX: 0,
lastY: 0,
};
function getPos(evt) {
const rect = canvas.getBoundingClientRect();
const scaleX = rect.width ? canvas.width / rect.width : 1;
const scaleY = rect.height ? canvas.height / rect.height : 1;
return {
x: (evt.clientX - rect.left) * scaleX,
y: (evt.clientY - rect.top) * scaleY,
};
}
function start(e) {
padState.drawing = true;
const pos = getPos(e);
padState.lastX = pos.x;
padState.lastY = pos.y;
}
function move(e) {
if (!padState.drawing) return;
const pos = getPos(e);
const x = pos.x;
const y = pos.y;
ctx.beginPath();
ctx.moveTo(padState.lastX, padState.lastY);
ctx.lineTo(x, y);
ctx.stroke();
padState.lastX = x;
padState.lastY = y;
}
function end() {
padState.drawing = false;
}
canvas.onpointerdown = start;
canvas.onpointermove = move;
canvas.onpointerup = end;
canvas.onpointerleave = end;
pointerPads.set(canvasId, { ctx, canvas });
},
registerDropHandler: (dotNetRef) => {
if (window.__pdfDropRegistered) return;
window.__pdfDropRegistered = true;
const prevent = (e) => {
e.preventDefault();
e.stopPropagation();
};
['dragenter', 'dragover', 'dragleave'].forEach(evt => {
document.addEventListener(evt, prevent, false);
});
document.addEventListener('drop', (e) => {
prevent(e);
const files = e.dataTransfer?.files;
if (!files || files.length === 0) {
return;
}
const file = files[0];
const reader = new FileReader();
reader.onload = () => {
const result = reader.result;
if (typeof result === 'string') {
const base64 = result.split(',')[1] || result;
dotNetRef?.invokeMethodAsync('LoadPdfFromBase64', base64);
}
};
reader.readAsDataURL(file);
}, false);
},
clearSignaturePad: (canvasId) => {
const pad = pointerPads.get(canvasId);
if (!pad) return;
pad.ctx.clearRect(0, 0, pad.canvas.width, pad.canvas.height);
},
getSignatureDataUrl: (canvasId) => {
const pad = pointerPads.get(canvasId);
if (!pad) return null;
return pad.canvas.toDataURL('image/png');
},
capturePointer: (element, pointerId) => {
if (element && element.setPointerCapture) {
try {
element.setPointerCapture(pointerId);
} catch (err) {
console.warn('capturePointer failed', err);
}
}
},
releasePointer: (element, pointerId) => {
if (element && element.releasePointerCapture) {
try {
element.releasePointerCapture(pointerId);
} catch (err) {
console.warn('releasePointer failed', err);
}
}
}
};
})();

View File

@@ -5,7 +5,6 @@ using DigitalData.EmailProfilerDispatcher.Abstraction.Entities;
using EnvelopeGenerator.Application.Common.Dto.EnvelopeReceiver;
using EnvelopeGenerator.Application.Common.Notifications.DocSigned;
using EnvelopeGenerator.Application.Common.Notifications.DocSigned.Handlers;
using EnvelopeGenerator.Domain.Entities;
using Microsoft.Extensions.DependencyInjection;
namespace EnvelopeGenerator.Tests.Application;
@@ -45,15 +44,9 @@ public class DocSignedNotificationTests : TestBase
// Create envelope receiver
var envRcv = this.CreateEnvelopeReceiver(env!.Id, rcv.Id);
var repo = GetRepository<EnvelopeReceiver>();
envRcv = await repo.CreateAsync(envRcv, cancel);
envRcv = await Repository.CreateAsync(envRcv, cancel);
var envRcvDto = _mapper.Map<EnvelopeReceiverDto>(envRcv);
var annots = Services.GetRequiredService<PsPdfKitAnnotation>();
var docSignedNtf = envRcvDto.ToDocSignedNotification(annots);
var docSignedNtf = envRcvDto.ToDocSignedNotification(new () { });
var sendSignedMailHandler = Host.Services.GetRequiredService<SendSignedMailHandler>();

View File

@@ -20,7 +20,7 @@
<ItemGroup>
<PackageReference Include="Bogus" Version="35.6.3" />
<PackageReference Include="coverlet.collector" Version="6.0.0" />
<PackageReference Include="DigitalData.Core.Abstraction.Application" Version="1.6.0" />
<PackageReference Include="DigitalData.Core.Abstraction.Application" Version="1.4.0" />
<PackageReference Include="DigitalData.Core.Abstractions" Version="4.3.0" />
<PackageReference Include="DigitalData.Core.API" Version="2.2.1" />
<PackageReference Include="DigitalData.Core.Application" Version="3.4.0" />

View File

@@ -1,10 +1,8 @@
using Bogus;
using CommandDotNet;
using DigitalData.Core.Abstraction.Application.Repository;
using DigitalData.UserManager.Domain.Entities;
using EnvelopeGenerator.Application;
using EnvelopeGenerator.Application.Common.Configurations;
using EnvelopeGenerator.Application.Common.Extensions;
using EnvelopeGenerator.Application.EnvelopeReceivers.Commands;
using EnvelopeGenerator.Application.Envelopes.Commands;
using EnvelopeGenerator.Application.Histories.Commands;
@@ -13,7 +11,6 @@ using EnvelopeGenerator.Application.Users.Commands;
using EnvelopeGenerator.Domain.Constants;
using EnvelopeGenerator.Domain.Entities;
using EnvelopeGenerator.Infrastructure;
using EnvelopeGenerator.Tests.Application;
using MediatR;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Configuration;
@@ -23,6 +20,7 @@ using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Options;
using QuestPDF.Fluent;
using QuestPDF.Infrastructure;
using EnvelopeGenerator.Application.Common.Extensions;
namespace EnvelopeGenerator.Tests.Application;
@@ -44,29 +42,10 @@ public class Fake
// add Application and Infrastructure services
#pragma warning disable CS0618
services.AddEnvelopeGeneratorServices(configuration);
var cnnStrName = "Default";
var connStr = context.Configuration.GetConnectionString(cnnStrName)
?? throw new InvalidOperationException($"Connection string '{cnnStrName}' is missing in the application configuration.");
services.AddEnvelopeGeneratorInfrastructureServices(opt =>
{
opt.AddDbContext(dbCtxOpt => dbCtxOpt.UseInMemoryDatabase("EnvelopeGeneratorTestDb"));
opt.AddDbTriggerParams(context.Configuration);
opt.AddDbContext((provider, options) =>
{
var logger = provider.GetRequiredService<ILogger<EGDbContext>>();
options.UseSqlServer(connStr)
.LogTo(log => logger.LogInformation("{log}", log), LogLevel.Trace)
.EnableSensitiveDataLogging()
.EnableDetailedErrors();
});
opt.AddSQLExecutor(executor => executor.ConnectionString = connStr);
});
services.AddEnvelopeGeneratorInfrastructureServices(
(sp, options) => options.UseInMemoryDatabase("EnvelopeGeneratorTestDb"),
context.Configuration
);
var prodCnnStr = context.Configuration.GetConnectionString("Default");
services.AddDbContext<EGDbContext2Prod>(opt => opt.UseSqlServer(prodCnnStr));

View File

@@ -24,8 +24,6 @@ public abstract class TestBase : Faker
protected IRepository Repository => Host.Services.GetRequiredService<IRepository>();
protected IServiceProvider Services => Host.Services;
protected abstract void ConfigureServices(IServiceCollection services);
[SetUp]
@@ -34,11 +32,9 @@ public abstract class TestBase : Faker
Host = Fake.CreateHost(ConfigureServices);
await Host.AddSamples();
var repo = GetRepository<EmailTemplate>();
// Add seed email templates
foreach (var temp in SeedEmailTemplates)
await repo.CreateAsync(temp);
await Repository.CreateAsync(temp);
}
[TearDown]

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

@@ -0,0 +1,52 @@
{
"homepage": "https://slavik0329.github.io/pdf-sign",
"name": "pdf-sign",
"version": "0.1.0",
"private": true,
"dependencies": {
"@testing-library/jest-dom": "^5.11.4",
"@testing-library/react": "^11.1.0",
"@testing-library/user-event": "^12.1.10",
"annotpdf": "^1.0.12",
"dayjs": "^1.9.6",
"pdf-lib": "^1.12.0",
"react": "^17.0.1",
"react-dom": "^17.0.1",
"react-draggable": "^4.4.3",
"react-dropzone": "^11.2.4",
"react-icons": "^4.1.0",
"react-pdf": "^5.0.0",
"react-scripts": "4.0.1",
"react-signature-canvas": "^1.0.3",
"web-vitals": "^0.2.4"
},
"scripts": {
"start": "react-scripts start",
"build": "react-scripts build",
"test": "react-scripts test",
"eject": "react-scripts eject",
"predeploy": "npm run build",
"deploy": "gh-pages -d build"
},
"eslintConfig": {
"extends": [
"react-app",
"react-app/jest"
]
},
"browserslist": {
"production": [
">0.2%",
"not dead",
"not op_mini all"
],
"development": [
"last 1 chrome version",
"last 1 firefox version",
"last 1 safari version"
]
},
"devDependencies": {
"gh-pages": "^3.1.0"
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.8 KiB

View File

@@ -0,0 +1,44 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8" />
<link rel="icon" href="%PUBLIC_URL%/favicon.ico" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<meta name="theme-color" content="#000000" />
<meta
name="description"
content="Finally, a simple way to put your signature on a PDF for free."
/>
<link rel="apple-touch-icon" href="%PUBLIC_URL%/logo192.png" />
<!--
manifest.json provides metadata used when your web app is installed on a
user's mobile device or desktop. See https://developers.google.com/web/fundamentals/web-app-manifest/
-->
<link rel="manifest" href="%PUBLIC_URL%/manifest.json" />
<!--
Notice the use of %PUBLIC_URL% in the tags above.
It will be replaced with the URL of the `public` folder during the build.
Only files inside the `public` folder can be referenced from the HTML.
Unlike "/favicon.ico" or "favicon.ico", "%PUBLIC_URL%/favicon.ico" will
work correctly both with client-side routing and a non-root public URL.
Learn how to configure a non-root public URL by running `npm run build`.
-->
<title>PDF-sign</title>
<link href="https://fonts.googleapis.com/css2?family=Open+Sans:wght@300;400;600;800&display=swap" rel="stylesheet">
</head>
<body>
<noscript>You need to enable JavaScript to run this app.</noscript>
<div id="root"></div>
<!--
This HTML file is a template.
If you open it directly in the browser, you will see an empty page.
You can add webfonts, meta tags, or analytics to this file.
The build step will place the bundled scripts into the <body> tag.
To begin the development, run `npm start` or `yarn start`.
To create a production bundle, use `npm run build` or `yarn build`.
-->
</body>
</html>

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.4 KiB

View File

@@ -0,0 +1,25 @@
{
"short_name": "React App",
"name": "Create React App Sample",
"icons": [
{
"src": "favicon.ico",
"sizes": "64x64 32x32 24x24 16x16",
"type": "image/x-icon"
},
{
"src": "logo192.png",
"type": "image/png",
"sizes": "192x192"
},
{
"src": "logo512.png",
"type": "image/png",
"sizes": "512x512"
}
],
"start_url": ".",
"display": "standalone",
"theme_color": "#000000",
"background_color": "#ffffff"
}

View File

@@ -0,0 +1,10 @@
body {
font-family: "Open Sans";
}
input:focus,
select:focus,
textarea:focus,
button:focus {
outline: none;
}

View File

@@ -0,0 +1,278 @@
import "./App.css";
import { useRef, useState } from "react";
import Drop from "./Drop";
import { Document, Page, pdfjs } from "react-pdf";
import { PDFDocument, rgb } from "pdf-lib";
import { blobToURL } from "./utils/Utils";
import PagingControl from "./components/PagingControl";
import { AddSigDialog } from "./components/AddSigDialog";
import { Header } from "./Header";
import { BigButton } from "./components/BigButton";
import DraggableSignature from "./components/DraggableSignature";
import DraggableText from "./components/DraggableText";
import dayjs from "dayjs";
pdfjs.GlobalWorkerOptions.workerSrc = `//cdnjs.cloudflare.com/ajax/libs/pdf.js/${pdfjs.version}/pdf.worker.js`;
function downloadURI(uri, name) {
var link = document.createElement("a");
link.download = name;
link.href = uri;
document.body.appendChild(link);
link.click();
document.body.removeChild(link);
}
function App() {
const styles = {
container: {
maxWidth: 900,
margin: "0 auto",
},
sigBlock: {
display: "inline-block",
border: "1px solid #000",
},
documentBlock: {
maxWidth: 800,
margin: "20px auto",
marginTop: 8,
border: "1px solid #999",
},
controls: {
maxWidth: 800,
margin: "0 auto",
marginTop: 8,
},
};
const [pdf, setPdf] = useState(null);
const [autoDate, setAutoDate] = useState(true);
const [signatureURL, setSignatureURL] = useState(null);
const [position, setPosition] = useState(null);
const [signatureDialogVisible, setSignatureDialogVisible] = useState(false);
const [textInputVisible, setTextInputVisible] = useState(false);
const [pageNum, setPageNum] = useState(0);
const [totalPages, setTotalPages] = useState(0);
const [pageDetails, setPageDetails] = useState(null);
const documentRef = useRef(null);
return (
<div>
<Header />
<div style={styles.container}>
{signatureDialogVisible ? (
<AddSigDialog
autoDate={autoDate}
setAutoDate={setAutoDate}
onClose={() => setSignatureDialogVisible(false)}
onConfirm={(url) => {
setSignatureURL(url);
setSignatureDialogVisible(false);
}}
/>
) : null}
{!pdf ? (
<Drop
onLoaded={async (files) => {
const URL = await blobToURL(files[0]);
setPdf(URL);
}}
/>
) : null}
{pdf ? (
<div>
<div style={styles.controls}>
{!signatureURL ? (
<BigButton
marginRight={8}
title={"Add signature"}
onClick={() => setSignatureDialogVisible(true)}
/>
) : null}
<BigButton
marginRight={8}
title={"Add Date"}
onClick={() => setTextInputVisible("date")}
/>
<BigButton
marginRight={8}
title={"Add Text"}
onClick={() => setTextInputVisible(true)}
/>
<BigButton
marginRight={8}
title={"Reset"}
onClick={() => {
setTextInputVisible(false);
setSignatureDialogVisible(false);
setSignatureURL(null);
setPdf(null);
setTotalPages(0);
setPageNum(0);
setPageDetails(null);
}}
/>
{pdf ? (
<BigButton
marginRight={8}
inverted={true}
title={"Download"}
onClick={() => {
downloadURI(pdf, "file.pdf");
}}
/>
) : null}
</div>
<div ref={documentRef} style={styles.documentBlock}>
{textInputVisible ? (
<DraggableText
initialText={
textInputVisible === "date"
? dayjs().format("M/d/YYYY")
: null
}
onCancel={() => setTextInputVisible(false)}
onEnd={setPosition}
onSet={async (text) => {
const { originalHeight, originalWidth } = pageDetails;
const scale = originalWidth / documentRef.current.clientWidth;
const y =
documentRef.current.clientHeight -
(position.y +
(12 * scale) -
position.offsetY -
documentRef.current.offsetTop);
const x =
position.x -
166 -
position.offsetX -
documentRef.current.offsetLeft;
// new XY in relation to actual document size
const newY =
(y * originalHeight) / documentRef.current.clientHeight;
const newX =
(x * originalWidth) / documentRef.current.clientWidth;
const pdfDoc = await PDFDocument.load(pdf);
const pages = pdfDoc.getPages();
const firstPage = pages[pageNum];
firstPage.drawText(text, {
x: newX,
y: newY,
size: 20 * scale,
});
const pdfBytes = await pdfDoc.save();
const blob = new Blob([new Uint8Array(pdfBytes)]);
const URL = await blobToURL(blob);
setPdf(URL);
setPosition(null);
setTextInputVisible(false);
}}
/>
) : null}
{signatureURL ? (
<DraggableSignature
url={signatureURL}
onCancel={() => {
setSignatureURL(null);
}}
onSet={async () => {
const { originalHeight, originalWidth } = pageDetails;
const scale = originalWidth / documentRef.current.clientWidth;
const y =
documentRef.current.clientHeight -
(position.y -
position.offsetY +
64 -
documentRef.current.offsetTop);
const x =
position.x -
160 -
position.offsetX -
documentRef.current.offsetLeft;
// new XY in relation to actual document size
const newY =
(y * originalHeight) / documentRef.current.clientHeight;
const newX =
(x * originalWidth) / documentRef.current.clientWidth;
const pdfDoc = await PDFDocument.load(pdf);
const pages = pdfDoc.getPages();
const firstPage = pages[pageNum];
const pngImage = await pdfDoc.embedPng(signatureURL);
const pngDims = pngImage.scale( scale * .3);
firstPage.drawImage(pngImage, {
x: newX,
y: newY,
width: pngDims.width,
height: pngDims.height,
});
if (autoDate) {
firstPage.drawText(
`Signed ${dayjs().format(
"M/d/YYYY HH:mm:ss ZZ"
)}`,
{
x: newX,
y: newY - 10,
size: 14 * scale,
color: rgb(0.074, 0.545, 0.262),
}
);
}
const pdfBytes = await pdfDoc.save();
const blob = new Blob([new Uint8Array(pdfBytes)]);
const URL = await blobToURL(blob);
setPdf(URL);
setPosition(null);
setSignatureURL(null);
}}
onEnd={setPosition}
/>
) : null}
<Document
file={pdf}
onLoadSuccess={(data) => {
setTotalPages(data.numPages);
}}
>
<Page
pageNumber={pageNum + 1}
width={800}
height={1200}
onLoadSuccess={(data) => {
setPageDetails(data);
}}
/>
</Document>
</div>
<PagingControl
pageNum={pageNum}
setPageNum={setPageNum}
totalPages={totalPages}
/>
</div>
) : null}
</div>
</div>
);
}
export default App;

View File

@@ -0,0 +1,8 @@
import { render, screen } from '@testing-library/react';
import App from './App';
test('renders learn react link', () => {
render(<App />);
const linkElement = screen.getByText(/learn react/i);
expect(linkElement).toBeInTheDocument();
});

View File

@@ -0,0 +1,37 @@
import React, { useCallback } from "react";
import { useDropzone } from "react-dropzone";
import { cleanBorder, primary45 } from "./utils/colors";
export default function Drop({ onLoaded }) {
const styles = {
container: {
textAlign: "center",
border: cleanBorder,
padding: 20,
marginTop: 12,
color: primary45,
fontSize: 18,
fontWeight: 600,
borderRadius: 4,
userSelect: "none",
outline: 0,
cursor: "pointer",
},
};
const onDrop = useCallback((acceptedFiles) => {
onLoaded(acceptedFiles);
// Do something with the files
}, []);
const { getRootProps, getInputProps, isDragActive } = useDropzone({
onDrop,
accept: "application/pdf",
});
return (
<div {...getRootProps()} style={styles.container}>
<input {...getInputProps()} />
{isDragActive ? <p>Drop a PDF here</p> : <p>Drag a PDF here</p>}
</div>
);
}

View File

@@ -0,0 +1,15 @@
import {primary45} from "./utils/colors";
export function Header() {
const styles = {
container: {
backgroundColor: primary45,
color: '#FFF',
padding: 12,
fontWeight: 600,
}
}
return <div style={styles.container}>
<div>Open PDF Sign</div>
</div>
}

View File

@@ -0,0 +1,77 @@
import { Dialog } from "./Dialog";
import SignatureCanvas from "react-signature-canvas";
import { ConfirmOrCancel } from "./ConfirmOrCancel";
import { primary45 } from "../utils/colors";
import { useRef } from "react";
export function AddSigDialog({ onConfirm, onClose, autoDate, setAutoDate }) {
const sigRef = useRef(null);
const styles = {
sigContainer: {
display: "flex",
justifyContent: "center",
},
sigBlock: {
display: "inline-block",
border: `1px solid ${primary45}`,
},
instructions: {
display: "flex",
justifyContent: "space-between",
textAlign: "center",
color: primary45,
marginTop: 8,
width: 600,
alignSelf: "center",
},
instructionsContainer: {
display: "flex",
justifyContent: "center",
},
};
return (
<Dialog
isVisible={true}
title={"Add signature"}
body={
<div style={styles.container}>
<div style={styles.sigContainer}>
<div style={styles.sigBlock}>
<SignatureCanvas
velocityFilterWeight={1}
ref={sigRef}
canvasProps={{
width: "600",
height: 200,
className: "sigCanvas",
}}
/>
</div>
</div>
<div style={styles.instructionsContainer}>
<div style={styles.instructions}>
<div>
Auto date/time{" "}
<input
type={"checkbox"}
checked={autoDate}
onChange={(e) => setAutoDate(e.target.checked)}
/>
</div>
<div>Draw your signature above</div>
</div>
</div>
<ConfirmOrCancel
onCancel={onClose}
onConfirm={() => {
const sigURL = sigRef.current.toDataURL();
onConfirm(sigURL);
}}
/>
</div>
}
/>
);
}

View File

@@ -0,0 +1,81 @@
import React from "react";
import { primary45 } from "../utils/colors";
import useHover from "../hooks/useHover";
export function BigButton({
title,
onClick,
inverted,
fullWidth,
customFillColor,
customWhiteColor,
style,
noHover,
id,
small,
disabled,
marginRight,
}) {
const [hoverRef, isHovered] = useHover();
let fillColor = customFillColor || primary45;
const whiteColor = customWhiteColor || "#FFF";
let initialBg = null;
let hoverBg = fillColor;
let initialColor = fillColor;
let hoverColor = whiteColor;
if (inverted) {
initialBg = fillColor;
hoverBg = null;
initialColor = whiteColor;
hoverColor = fillColor;
}
if (disabled) {
initialBg = "#ddd";
hoverBg = "#ddd";
fillColor = "#ddd";
}
const styles = {
container: {
display: "inline-flex",
alignItems: "center",
justifyContent: "center",
width: fullWidth ? "100%" : null,
backgroundColor: isHovered && !noHover ? hoverBg : initialBg,
color:
isHovered && !noHover && !disabled
? hoverColor
: disabled
? "#999"
: initialColor,
borderRadius: 4,
padding: small ? "2px 4px" : "6px 8px",
fontSize: small ? 14 : null,
border: `1px solid ${fillColor}`,
cursor: !disabled ? "pointer" : null,
userSelect: "none",
boxSizing: "border-box",
marginRight,
},
};
return (
<div
id={id}
ref={hoverRef}
style={{ ...styles.container, ...style }}
onClick={() => {
if (!disabled) {
onClick();
}
}}
>
{title}
</div>
);
}

View File

@@ -0,0 +1,37 @@
import { BigButton } from "./BigButton";
import React from "react";
export function ConfirmOrCancel({
onCancel,
onConfirm,
confirmTitle = "Confirm",
leftBlock,
hideCancel,
disabled
}) {
const styles = {
actions: {
display: "flex",
justifyContent: "space-between",
},
cancel: {
marginRight: 8,
},
};
return (
<div style={styles.actions}>
<div>{leftBlock}</div>
<div>
{!hideCancel ? (
<BigButton
title={"Cancel"}
style={styles.cancel}
onClick={onCancel}
/>
) : null}
<BigButton title={confirmTitle} inverted={true} onClick={onConfirm} disabled={disabled}/>
</div>
</div>
);
}

View File

@@ -0,0 +1,56 @@
import React from 'react';
import {primary45} from '../utils/colors';
import {FaTimes} from 'react-icons/fa';
import {Modal} from './Modal';
export function Dialog({
isVisible,
body,
onClose,
title,
noPadding,
backgroundColor,
positionTop,
style,
}) {
if (!isVisible) {
return null;
}
const styles = {
header: {
backgroundColor: primary45,
color: '#FFF',
padding: 8,
fontSize: 14,
display: 'flex',
justifyContent: 'space-between',
alignItems: 'center',
},
body: {
padding: noPadding ? 0 : 14,
backgroundColor: backgroundColor ? backgroundColor : '#FFF',
},
xIcon: {
cursor: 'pointer',
},
};
return (
<Modal onClose={onClose} isVisible={isVisible} positionTop={positionTop} style={style}>
<div style={styles.container}>
<div style={styles.header}>
<div>{title}</div>
<FaTimes
color={'#FFF'}
size={16}
style={styles.xIcon}
className={'dialogClose'}
onClick={onClose}
/>
</div>
<div style={styles.body}>{body}</div>
</div>
</Modal>
);
}

View File

@@ -0,0 +1,37 @@
import Draggable from "react-draggable";
import {BigButton} from "./BigButton"; // The default
import {FaCheck, FaTimes} from 'react-icons/fa'
import {cleanBorder, errorColor, goodColor, primary45} from "../utils/colors";
export default function DraggableSignature({ url, onEnd, onSet, onCancel }) {
const styles = {
container: {
position: 'absolute',
zIndex: 100000,
border: `2px solid ${primary45}`,
},
controls: {
position: 'absolute',
right: 0,
display: 'inline-block',
backgroundColor: primary45,
// borderRadius: 4,
},
smallButton: {
display: 'inline-block',
cursor: 'pointer',
padding: 4,
}
}
return (
<Draggable onStop={onEnd}>
<div style={styles.container}>
<div style={styles.controls}>
<div style={styles.smallButton} onClick={onSet}><FaCheck color={goodColor}/></div>
<div style={styles.smallButton} onClick={onCancel}><FaTimes color={errorColor}/></div>
</div>
<img src={url} width={200} style={styles.img} draggable={false} />
</div>
</Draggable>
);
}

View File

@@ -0,0 +1,66 @@
import Draggable from "react-draggable";
import { FaCheck, FaTimes } from "react-icons/fa";
import { cleanBorder, errorColor, goodColor, primary45 } from "../utils/colors";
import { useState, useEffect, useRef } from "react";
export default function DraggableText({ onEnd, onSet, onCancel, initialText }) {
const [text, setText] = useState("Text");
const inputRef = useRef(null);
useEffect(() => {
if (initialText) {
setText(initialText)
} else {
inputRef.current.focus();
inputRef.current.select()
}
}, [])
const styles = {
container: {
position: "absolute",
zIndex: 100000,
border: `2px solid ${primary45}`,
},
controls: {
position: "absolute",
right: 0,
display: "inline-block",
backgroundColor: primary45,
// borderRadius: 4,
},
smallButton: {
display: "inline-block",
cursor: "pointer",
padding: 4,
},
input: {
border: 0,
fontSize: 20,
padding: 3,
backgroundColor: 'rgba(0,0,0,0)',
cursor: 'move'
}
};
return (
<Draggable onStop={onEnd}>
<div style={styles.container}>
<div style={styles.controls}>
<div style={styles.smallButton} onClick={()=>onSet(text)}>
<FaCheck color={goodColor} />
</div>
<div style={styles.smallButton} onClick={onCancel}>
<FaTimes color={errorColor} />
</div>
</div>
<input
ref={inputRef}
style={styles.input}
value={text}
placeholder={'Text'}
onChange={(e) => setText(e.target.value)}
/>
</div>
</Draggable>
);
}

View File

@@ -0,0 +1,43 @@
import React from 'react';
import {primary45} from '../utils/colors';
import {useIsSmallScreen} from '../hooks/useIsSmallScreen';
export function Modal({onClose, children, isVisible, style, positionTop}) {
const isSmallScreen = useIsSmallScreen();
const styles = {
container: {
position: isSmallScreen ? 'fixed' : 'absolute',
backgroundColor: '#FFF',
border: `1px solid ${primary45}`,
borderRadius: 4,
top: positionTop ? positionTop : isSmallScreen ? 60 : 150,
left: '50%',
transform: 'translateX(-50%)',
width: '94%',
fontFamily: 'Open Sans',
zIndex: 10000,
boxShadow: '0 0px 14px hsla(0, 0%, 0%, 0.2)',
},
background: {
position: 'fixed',
width: '100%',
height: '100%',
top: 0,
left: 0,
backgroundColor: '#00000033',
zIndex: 5000,
},
};
if (!isVisible) {
return null;
}
return (
<div style={styles.outer}>
<div style={styles.background} onClick={onClose} />
<div style={{...styles.container, ...style}}>{children}</div>
</div>
);
}

View File

@@ -0,0 +1,40 @@
import { BigButton } from "./BigButton";
import {primary45} from "../utils/colors";
export default function PagingControl({totalPages, pageNum, setPageNum}) {
const styles= {
container: {
marginTop: 8,
marginBottom: 8,
},
inlineFlex: {
display: 'flex',
justifyContent: 'center',
alignItems: 'center'
},
pageInfo: {
padding: 8,
color: primary45,
fontSize: 14,
}
}
return (
<div style={styles.container}>
<div style={styles.inlineFlex}>
<BigButton
title={"<"}
onClick={() => setPageNum(pageNum - 1)}
disabled={pageNum-1===-1}
/>
<div style={styles.pageInfo}>
Page: {pageNum + 1}/{totalPages}
</div>
<BigButton
title={">"}
onClick={() => setPageNum(pageNum + 1)}
disabled={pageNum+1>totalPages-1}
/>
</div>
</div>
);
}

View File

@@ -0,0 +1,29 @@
import React, {useCallback, useRef, useState} from 'react';
export default function useHover() {
const [value, setValue] = useState(false);
const handleMouseOver = useCallback(() => setValue(true), []);
const handleMouseOut = useCallback(() => setValue(false), []);
const ref = useRef();
const callbackRef = useCallback(
(node) => {
if (ref.current) {
ref.current.removeEventListener('mouseenter', handleMouseOver);
ref.current.removeEventListener('mouseleave', handleMouseOut);
}
ref.current = node;
if (ref.current) {
ref.current.addEventListener('mouseenter', handleMouseOver);
ref.current.addEventListener('mouseleave', handleMouseOut);
}
},
[handleMouseOver, handleMouseOut],
);
return [callbackRef, value];
}

View File

@@ -0,0 +1,6 @@
import {useWindowSize} from './useWindowSize';
export function useIsSmallScreen() {
const windowSize = useWindowSize();
return windowSize.width < 600;
}

View File

@@ -0,0 +1,29 @@
import React, {useState, useEffect} from 'react';
export function useWindowSize() {
const isClient = typeof window === 'object';
function getSize() {
return {
width: isClient ? window.innerWidth : undefined,
height: isClient ? window.innerHeight : undefined,
};
}
const [windowSize, setWindowSize] = useState(getSize);
useEffect(() => {
if (!isClient) {
return false;
}
function handleResize() {
setWindowSize(getSize());
}
window.addEventListener('resize', handleResize);
return () => window.removeEventListener('resize', handleResize);
}, []); // Empty array ensures that effect is only run on mount and unmount
return windowSize;
}

View File

@@ -0,0 +1,13 @@
body {
margin: 0;
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Oxygen',
'Ubuntu', 'Cantarell', 'Fira Sans', 'Droid Sans', 'Helvetica Neue',
sans-serif;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
}
code {
font-family: source-code-pro, Menlo, Monaco, Consolas, 'Courier New',
monospace;
}

View File

@@ -0,0 +1,17 @@
import React from 'react';
import ReactDOM from 'react-dom';
import './index.css';
import App from './App';
import reportWebVitals from './reportWebVitals';
ReactDOM.render(
<React.StrictMode>
<App />
</React.StrictMode>,
document.getElementById('root')
);
// If you want to start measuring performance in your app, pass a function
// to log results (for example: reportWebVitals(console.log))
// or send to an analytics endpoint. Learn more: https://bit.ly/CRA-vitals
reportWebVitals();

View File

@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 841.9 595.3"><g fill="#61DAFB"><path d="M666.3 296.5c0-32.5-40.7-63.3-103.1-82.4 14.4-63.6 8-114.2-20.2-130.4-6.5-3.8-14.1-5.6-22.4-5.6v22.3c4.6 0 8.3.9 11.4 2.6 13.6 7.8 19.5 37.5 14.9 75.7-1.1 9.4-2.9 19.3-5.1 29.4-19.6-4.8-41-8.5-63.5-10.9-13.5-18.5-27.5-35.3-41.6-50 32.6-30.3 63.2-46.9 84-46.9V78c-27.5 0-63.5 19.6-99.9 53.6-36.4-33.8-72.4-53.2-99.9-53.2v22.3c20.7 0 51.4 16.5 84 46.6-14 14.7-28 31.4-41.3 49.9-22.6 2.4-44 6.1-63.6 11-2.3-10-4-19.7-5.2-29-4.7-38.2 1.1-67.9 14.6-75.8 3-1.8 6.9-2.6 11.5-2.6V78.5c-8.4 0-16 1.8-22.6 5.6-28.1 16.2-34.4 66.7-19.9 130.1-62.2 19.2-102.7 49.9-102.7 82.3 0 32.5 40.7 63.3 103.1 82.4-14.4 63.6-8 114.2 20.2 130.4 6.5 3.8 14.1 5.6 22.5 5.6 27.5 0 63.5-19.6 99.9-53.6 36.4 33.8 72.4 53.2 99.9 53.2 8.4 0 16-1.8 22.6-5.6 28.1-16.2 34.4-66.7 19.9-130.1 62-19.1 102.5-49.9 102.5-82.3zm-130.2-66.7c-3.7 12.9-8.3 26.2-13.5 39.5-4.1-8-8.4-16-13.1-24-4.6-8-9.5-15.8-14.4-23.4 14.2 2.1 27.9 4.7 41 7.9zm-45.8 106.5c-7.8 13.5-15.8 26.3-24.1 38.2-14.9 1.3-30 2-45.2 2-15.1 0-30.2-.7-45-1.9-8.3-11.9-16.4-24.6-24.2-38-7.6-13.1-14.5-26.4-20.8-39.8 6.2-13.4 13.2-26.8 20.7-39.9 7.8-13.5 15.8-26.3 24.1-38.2 14.9-1.3 30-2 45.2-2 15.1 0 30.2.7 45 1.9 8.3 11.9 16.4 24.6 24.2 38 7.6 13.1 14.5 26.4 20.8 39.8-6.3 13.4-13.2 26.8-20.7 39.9zm32.3-13c5.4 13.4 10 26.8 13.8 39.8-13.1 3.2-26.9 5.9-41.2 8 4.9-7.7 9.8-15.6 14.4-23.7 4.6-8 8.9-16.1 13-24.1zM421.2 430c-9.3-9.6-18.6-20.3-27.8-32 9 .4 18.2.7 27.5.7 9.4 0 18.7-.2 27.8-.7-9 11.7-18.3 22.4-27.5 32zm-74.4-58.9c-14.2-2.1-27.9-4.7-41-7.9 3.7-12.9 8.3-26.2 13.5-39.5 4.1 8 8.4 16 13.1 24 4.7 8 9.5 15.8 14.4 23.4zM420.7 163c9.3 9.6 18.6 20.3 27.8 32-9-.4-18.2-.7-27.5-.7-9.4 0-18.7.2-27.8.7 9-11.7 18.3-22.4 27.5-32zm-74 58.9c-4.9 7.7-9.8 15.6-14.4 23.7-4.6 8-8.9 16-13 24-5.4-13.4-10-26.8-13.8-39.8 13.1-3.1 26.9-5.8 41.2-7.9zm-90.5 125.2c-35.4-15.1-58.3-34.9-58.3-50.6 0-15.7 22.9-35.6 58.3-50.6 8.6-3.7 18-7 27.7-10.1 5.7 19.6 13.2 40 22.5 60.9-9.2 20.8-16.6 41.1-22.2 60.6-9.9-3.1-19.3-6.5-28-10.2zM310 490c-13.6-7.8-19.5-37.5-14.9-75.7 1.1-9.4 2.9-19.3 5.1-29.4 19.6 4.8 41 8.5 63.5 10.9 13.5 18.5 27.5 35.3 41.6 50-32.6 30.3-63.2 46.9-84 46.9-4.5-.1-8.3-1-11.3-2.7zm237.2-76.2c4.7 38.2-1.1 67.9-14.6 75.8-3 1.8-6.9 2.6-11.5 2.6-20.7 0-51.4-16.5-84-46.6 14-14.7 28-31.4 41.3-49.9 22.6-2.4 44-6.1 63.6-11 2.3 10.1 4.1 19.8 5.2 29.1zm38.5-66.7c-8.6 3.7-18 7-27.7 10.1-5.7-19.6-13.2-40-22.5-60.9 9.2-20.8 16.6-41.1 22.2-60.6 9.9 3.1 19.3 6.5 28.1 10.2 35.4 15.1 58.3 34.9 58.3 50.6-.1 15.7-23 35.6-58.4 50.6zM320.8 78.4z"/><circle cx="420.9" cy="296.5" r="45.7"/><path d="M520.5 78.1z"/></g></svg>

After

Width:  |  Height:  |  Size: 2.6 KiB

View File

@@ -0,0 +1,13 @@
const reportWebVitals = onPerfEntry => {
if (onPerfEntry && onPerfEntry instanceof Function) {
import('web-vitals').then(({ getCLS, getFID, getFCP, getLCP, getTTFB }) => {
getCLS(onPerfEntry);
getFID(onPerfEntry);
getFCP(onPerfEntry);
getLCP(onPerfEntry);
getTTFB(onPerfEntry);
});
}
};
export default reportWebVitals;

View File

@@ -0,0 +1,5 @@
// jest-dom adds custom jest matchers for asserting on DOM nodes.
// allows you to do things like:
// expect(element).toHaveTextContent(/react/i)
// learn more: https://github.com/testing-library/jest-dom
import '@testing-library/jest-dom';

View File

@@ -0,0 +1,27 @@
export function blobToURL(blob) {
return new Promise((resolve) => {
const reader = new FileReader();
reader.readAsDataURL(blob);
reader.onloadend = function () {
const base64data = reader.result;
resolve(base64data);
};
});
}
export async function fileToBlob(file, handleUpdate) {
const { content, size } = file;
let chunks = [];
let i = 0;
const totalCount = Math.round(size / 250000);
for await (const chunk of content) {
if (handleUpdate) {
handleUpdate(i, totalCount);
}
chunks.push(chunk);
i++;
}
// eslint-disable-next-line no-undef
return new Blob(chunks);
}

View File

@@ -0,0 +1,28 @@
export const primary = '#2b6284';
export const primary2 = '#ecf4f9';
export const primary3 = '#9fc7e0';
export const primary35 = '#97bace';
export const primary4 = 'hsl(204,38%,55%)';
export const primary45 = 'hsl(218,49%,66%)';
export const primary46 = '#6778cb';
export const primary15 = 'rgb(241 249 255)';
export const primary5 = '#3881ad';
export const primary6 = '#132b3a';
// export const primary = '#666';
// export const primary2 = '#EEE';
// export const primary3 = '#CCC';
// export const primary4 = '#AAA';
// export const primary5 = '#888';
// export const primary6 = '#333';
export const primary16 = 'hsl(208 100% 96% / 1)';
export const errorColor = '#ef6565';
export const lightErrorColor = '#ef9c9c';
export const goodColor = '#53c171';
export const cleanBorder = '1px solid rgb(208, 227, 239)';
export const lightBorder = 'hsl(203 51% 80% / 1)';

File diff suppressed because it is too large Load Diff

View File

@@ -44,7 +44,7 @@ public class AnnotationController : ControllerBase
[Authorize(Roles = ReceiverRole.FullyAuth)]
[HttpPost]
public async Task<IActionResult> CreateOrUpdate([FromBody] PsPdfKitAnnotation? psPdfKitAnnotation = null, CancellationToken cancel = default)
public async Task<IActionResult> CreateOrUpdate([FromBody] PsPdfKitAnnotation psPdfKitAnnotation, CancellationToken cancel = default)
{
// get claims
var signature = User.GetAuthReceiverSignature();
@@ -56,12 +56,6 @@ public class AnnotationController : ControllerBase
return Unauthorized("User authentication is incomplete. Missing required claims for processing this request.");
}
// check if non read-and-confirm envelope is signed without annotation
var er = await _mediator.ReadEnvelopeReceiverAsync(uuid, signature, cancel).ThrowIfNull(Exceptions.NotFound);
if (!er.Envelope!.ReadOnly && psPdfKitAnnotation is null)
return BadRequest();
// Again check if receiver has already signed
if (await _mediator.IsSignedAsync(uuid, signature, cancel))
return Problem(statusCode: 409);

View File

@@ -248,9 +248,7 @@ public class EnvelopeController : ViewControllerBase
{
if (er.Envelope!.Documents?.FirstOrDefault() is DocumentDto doc && doc.ByteData is not null)
{
using var pdf = er.Envelope.ReadOnly
? Pdf.FromMemory(doc.ByteData)
: Pdf.FromMemory(doc.ByteData).Background(doc.Elements!);
using var pdf = Pdf.FromMemory(doc.ByteData).Background(doc.Elements!);
doc.ByteData = pdf.ExportAsBytes();
@@ -264,8 +262,6 @@ public class EnvelopeController : ViewControllerBase
await HttpContext.SignInEnvelopeAsync(er, ReceiverRole.FullyAuth);
ViewData["ReadAndConfirm"] = er.Envelope.ReadOnly;
//add PSPDFKit licence key
ViewData["PSPDFKitLicenseKey"] = _configuration["PSPDFKitLicenseKey"];

View File

@@ -12,9 +12,9 @@
<PackageTags>digital data envelope generator web</PackageTags>
<Description>EnvelopeGenerator.Web is an ASP.NET MVC application developed to manage signing processes. It uses Entity Framework Core (EF Core) for database operations. The user interface for signing processes is developed with Razor View Engine (.cshtml files) and JavaScript under wwwroot, integrated with PSPDFKit. This integration allows users to view and sign documents seamlessly.</Description>
<ApplicationIcon>Assets\icon.ico</ApplicationIcon>
<Version>3.9.0</Version>
<AssemblyVersion>3.9.0</AssemblyVersion>
<FileVersion>3.9.0</FileVersion>
<Version>3.8.2</Version>
<AssemblyVersion>3.8.2</AssemblyVersion>
<FileVersion>3.8.2</FileVersion>
<Copyright>Copyright © 2025 Digital Data GmbH. All rights reserved.</Copyright>
</PropertyGroup>

View File

@@ -43,14 +43,12 @@
</svg>
<span>@_localizer.Reject()</span>
</button>
@if(!Model.Envelope!.ReadOnly){
<button class="btn_refresh btn btn-secondary btn-desktop" type="button">
<svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" fill="currentColor" class="bi bi-arrow-counterclockwise" viewBox="0 0 16 16">
<path fill-rule="evenodd" d="M8 3a5 5 0 1 1-4.546 2.914.5.5 0 0 0-.908-.417A6 6 0 1 0 8 2v1z" />
<path d="M8 4.466V.534a.25.25 0 0 0-.41-.192L5.23 2.308a.25.25 0 0 0 0 .384l2.36 1.966A.25.25 0 0 0 8 4.466z" />
</svg>
</button>
}
</div>
}
<div class="dd-cards-container">

View File

@@ -42,8 +42,6 @@
const IS_DESKTOP_SIZE = DEVICE_SCREEN_TYPE == 'desktop'
const IS_MOBILE_DEVICE = /Mobi|Android/i.test(window.navigator.userAgent);
const READ_AND_CONFIRM = @((ViewData["ReadAndConfirm"] is bool readAndConfirm && readAndConfirm).ToString().ToLower())
</script>
<script src="~/lib/jquery/dist/jquery.min.js"></script>

View File

@@ -35,18 +35,15 @@ class App {
});
// Load annotations into PSPDFKit
if (!READ_AND_CONFIRM)
try {
let signatures = await createAnnotations(this.currentDocument, this.envelopeReceiver.envelopeId, this.envelopeReceiver.receiverId);
await this.pdfKit.create(signatures);
} catch (e) {
console.error("Error loading annotations:", e);
}
try {
let signatures = await createAnnotations(this.currentDocument, this.envelopeReceiver.envelopeId, this.envelopeReceiver.receiverId);
await this.pdfKit.create(signatures);
} catch (e) {
console.error("Error loading annotations:", e);
}
//add click events of external buttons
if (!READ_AND_CONFIRM) {
[...document.getElementsByClassName('btn_refresh')].forEach(btn => btn.addEventListener('click', _ => this.handleClick('RESET')));
}
[...document.getElementsByClassName('btn_refresh')].forEach(btn => btn.addEventListener('click', _ => this.handleClick('RESET')));
[...document.getElementsByClassName('btn_complete')].forEach(btn => btn.addEventListener('click', _ => this.handleClick('FINISH')));
[...document.getElementsByClassName('btn_reject')].forEach(btn => btn.addEventListener('click', _ => this.handleClick('REJECT')));
}
@@ -185,70 +182,45 @@ class App {
}
async handleFinish(event) {
const iJSON = await this.pdfKit.exportInstantJSON()
let annotResult = undefined;
const iFormFieldValues = iJSON.formFieldValues;
// READ_AND_CONFIRM flow: require all pages viewed, skip annotation validations
if (READ_AND_CONFIRM) {
const allViewed = JSON.parse(sessionStorage.getItem('pspdf_all_pages_rendered') || 'false') === true
if (!allViewed) {
const unviewed = JSON.parse(sessionStorage.getItem('pspdf_unviewed_pages') || '[]')
const message = unviewed.length
? `Bitte sehen Sie sich die folgenden Seiten an: ${unviewed.join(', ')}`
: 'Bitte sehen Sie sich alle Seiten an.'
//check required
const iReqFields = iFormFieldValues.filter(f => isFieldRequired(f))
const hasEmptyReq = iReqFields.some(f => (f.value === undefined || f.value === null || f.value === ""))
await Swal.fire({
title: 'Warnung',
text: message,
icon: 'warning'
})
return false
}
if (hasEmptyReq) {
Swal.fire({
title: 'Warnung',
text: 'Bitte füllen Sie alle Standortinformationen vollständig aus!',
icon: 'warning',
})
return false;
}
else {
const iJSON = await this.pdfKit.exportInstantJSON()
const iFormFieldValues = iJSON.formFieldValues;
//check required
const iReqFields = iFormFieldValues.filter(f => isFieldRequired(f))
const hasEmptyReq = iReqFields.some(f => (f.value === undefined || f.value === null || f.value === ""))
if (hasEmptyReq) {
//check city
const city_regex = new RegExp("^[a-zA-Z\\u0080-\\u024F]+(?:([\\ \\-\\']|(\\.\\ ))[a-zA-Z\\u0080-\\u024F]+)*$")
const iCityFields = iFormFieldValues.filter(f => isCityField(f))
for (var f of iCityFields)
if (!IS_MOBILE_DEVICE && !city_regex.test(f.value)) {
Swal.fire({
title: 'Warnung',
text: 'Bitte füllen Sie alle Standortinformationen vollständig aus!',
text: `Bitte überprüfen Sie die eingegebene Ortsangabe "${f.value}" auf korrekte Formatierung. Beispiele für richtige Formate sind: München, Île-de-France, Sauðárkrókur, San Francisco, St. Catharines usw.`,
icon: 'warning',
})
return false;
}
//check city
const city_regex = new RegExp("^[a-zA-Z\\u0080-\\u024F]+(?:([\\ \\-\\']|(\\.\\ ))[a-zA-Z\\u0080-\\u024F]+)*$")
const iCityFields = iFormFieldValues.filter(f => isCityField(f))
for (var f of iCityFields)
if (!IS_MOBILE_DEVICE && !city_regex.test(f.value)) {
Swal.fire({
title: 'Warnung',
text: `Bitte überprüfen Sie die eingegebene Ortsangabe "${f.value}" auf korrekte Formatierung. Beispiele für richtige Formate sind: München, Île-de-France, Sauðárkrókur, San Francisco, St. Catharines usw.`,
icon: 'warning',
})
return false;
}
//check # of signature
const validationResult = await this.validateAnnotations(this.signatureCount)
if (validationResult === false) {
Swal.fire({
title: 'Warnung',
text: 'Es wurden nicht alle Signaturfelder ausgefüllt!',
icon: 'warning',
})
return false
}
// set annot-result if all validations passed
annotResult = { instant: iJSON, structured: mapSignature(iJSON) };
//check # of signature
const validationResult = await this.validateAnnotations(this.signatureCount)
if (validationResult === false) {
Swal.fire({
title: 'Warnung',
text: 'Es wurden nicht alle Signaturfelder ausgefüllt!',
icon: 'warning',
})
return false
}
return Swal.fire({
@@ -277,7 +249,10 @@ class App {
// Export annotation data and save to database
try {
const res = READ_AND_CONFIRM ? await signEnvelope() : await signEnvelope(annotResult);
const res = await signEnvelope({
instant: iJSON,
structured: mapSignature(iJSON)
});
if (!res.ok) {
if (res.status === 409) {

Some files were not shown because too many files have changed in this diff Show More