Compare commits
14 Commits
172f2e27d7
...
feat/recei
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
e6b41f10c8 | ||
|
|
dc78ad4a24 | ||
| b282318298 | |||
| 8d68ea8c57 | |||
|
|
7cb8b02b1d | ||
|
|
cc1d33462c | ||
|
|
cd85b4fffc | ||
|
|
ab3e7fb4e9 | ||
|
|
4a7676765c | ||
|
|
562ceb9c3f | ||
|
|
751ea706df | ||
|
|
490ad9f7cf | ||
| 7aeaba7c12 | |||
| 2301a81a1c |
1
.vscode/settings.json
vendored
Normal file
1
.vscode/settings.json
vendored
Normal file
@@ -0,0 +1 @@
|
||||
{}
|
||||
@@ -74,11 +74,6 @@ public record EnvelopeDto
|
||||
/// </summary>
|
||||
public int? EnvelopeTypeId { get; set; }
|
||||
|
||||
/// <summary>
|
||||
///
|
||||
/// </summary>
|
||||
public bool ReadOnly => EnvelopeTypeId == 2;
|
||||
|
||||
/// <summary>
|
||||
///
|
||||
/// </summary>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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" />
|
||||
|
||||
@@ -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">
|
||||
|
||||
@@ -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" />
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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) _
|
||||
|
||||
@@ -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" />
|
||||
|
||||
@@ -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; }
|
||||
|
||||
|
||||
@@ -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();
|
||||
|
||||
|
||||
@@ -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>
|
||||
|
||||
|
||||
@@ -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>
|
||||
@@ -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");
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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) { }
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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; }
|
||||
}
|
||||
@@ -1,8 +0,0 @@
|
||||
using System.Collections.Generic;
|
||||
|
||||
namespace EnvelopeGenerator.Jobs.FinalizeDocument;
|
||||
|
||||
public class ReportSource
|
||||
{
|
||||
public List<ReportItem> Items { get; set; } = new();
|
||||
}
|
||||
20
EnvelopeGenerator.ReceiverUI/Components/App.razor
Normal file
20
EnvelopeGenerator.ReceiverUI/Components/App.razor
Normal 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>
|
||||
@@ -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>
|
||||
@@ -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;
|
||||
}
|
||||
30
EnvelopeGenerator.ReceiverUI/Components/Layout/NavMenu.razor
Normal file
30
EnvelopeGenerator.ReceiverUI/Components/Layout/NavMenu.razor
Normal 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>
|
||||
|
||||
105
EnvelopeGenerator.ReceiverUI/Components/Layout/NavMenu.razor.css
Normal file
105
EnvelopeGenerator.ReceiverUI/Components/Layout/NavMenu.razor.css
Normal 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;
|
||||
}
|
||||
}
|
||||
19
EnvelopeGenerator.ReceiverUI/Components/Pages/Counter.razor
Normal file
19
EnvelopeGenerator.ReceiverUI/Components/Pages/Counter.razor
Normal 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++;
|
||||
}
|
||||
}
|
||||
36
EnvelopeGenerator.ReceiverUI/Components/Pages/Error.razor
Normal file
36
EnvelopeGenerator.ReceiverUI/Components/Pages/Error.razor
Normal 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;
|
||||
}
|
||||
7
EnvelopeGenerator.ReceiverUI/Components/Pages/Home.razor
Normal file
7
EnvelopeGenerator.ReceiverUI/Components/Pages/Home.razor
Normal file
@@ -0,0 +1,7 @@
|
||||
@page "/"
|
||||
|
||||
<PageTitle>Home</PageTitle>
|
||||
|
||||
<h1>Hello, world!</h1>
|
||||
|
||||
Welcome to your new app.
|
||||
64
EnvelopeGenerator.ReceiverUI/Components/Pages/Weather.razor
Normal file
64
EnvelopeGenerator.ReceiverUI/Components/Pages/Weather.razor
Normal 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);
|
||||
}
|
||||
}
|
||||
6
EnvelopeGenerator.ReceiverUI/Components/Routes.razor
Normal file
6
EnvelopeGenerator.ReceiverUI/Components/Routes.razor
Normal 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>
|
||||
10
EnvelopeGenerator.ReceiverUI/Components/_Imports.razor
Normal file
10
EnvelopeGenerator.ReceiverUI/Components/_Imports.razor
Normal 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
|
||||
@@ -0,0 +1,9 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk.Web">
|
||||
|
||||
<PropertyGroup>
|
||||
<TargetFramework>net8.0</TargetFramework>
|
||||
<Nullable>enable</Nullable>
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
</PropertyGroup>
|
||||
|
||||
</Project>
|
||||
27
EnvelopeGenerator.ReceiverUI/Program.cs
Normal file
27
EnvelopeGenerator.ReceiverUI/Program.cs
Normal 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();
|
||||
38
EnvelopeGenerator.ReceiverUI/Properties/launchSettings.json
Normal file
38
EnvelopeGenerator.ReceiverUI/Properties/launchSettings.json
Normal 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"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,8 @@
|
||||
{
|
||||
"Logging": {
|
||||
"LogLevel": {
|
||||
"Default": "Information",
|
||||
"Microsoft.AspNetCore": "Warning"
|
||||
}
|
||||
}
|
||||
}
|
||||
9
EnvelopeGenerator.ReceiverUI/appsettings.json
Normal file
9
EnvelopeGenerator.ReceiverUI/appsettings.json
Normal file
@@ -0,0 +1,9 @@
|
||||
{
|
||||
"Logging": {
|
||||
"LogLevel": {
|
||||
"Default": "Information",
|
||||
"Microsoft.AspNetCore": "Warning"
|
||||
}
|
||||
},
|
||||
"AllowedHosts": "*"
|
||||
}
|
||||
51
EnvelopeGenerator.ReceiverUI/wwwroot/app.css
Normal file
51
EnvelopeGenerator.ReceiverUI/wwwroot/app.css
Normal 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;
|
||||
}
|
||||
7
EnvelopeGenerator.ReceiverUI/wwwroot/bootstrap/bootstrap.min.css
vendored
Normal file
7
EnvelopeGenerator.ReceiverUI/wwwroot/bootstrap/bootstrap.min.css
vendored
Normal file
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
BIN
EnvelopeGenerator.ReceiverUI/wwwroot/favicon.png
Normal file
BIN
EnvelopeGenerator.ReceiverUI/wwwroot/favicon.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 1.1 KiB |
13
EnvelopeGenerator.ReceiverUIBlazor/App.razor
Normal file
13
EnvelopeGenerator.ReceiverUIBlazor/App.razor
Normal 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>
|
||||
@@ -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>
|
||||
428
EnvelopeGenerator.ReceiverUIBlazor/Pages/Index.razor
Normal file
428
EnvelopeGenerator.ReceiverUIBlazor/Pages/Index.razor
Normal 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)"><</button>
|
||||
<span>Page @DisplayPage / @PageCount</span>
|
||||
<button class="btn" @onclick="NextPage" disabled="@(!CanNext)">></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;
|
||||
}
|
||||
}
|
||||
11
EnvelopeGenerator.ReceiverUIBlazor/Program.cs
Normal file
11
EnvelopeGenerator.ReceiverUIBlazor/Program.cs
Normal 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();
|
||||
@@ -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>
|
||||
@@ -0,0 +1,12 @@
|
||||
{
|
||||
"profiles": {
|
||||
"EnvelopeGenerator.ReceiverUIBlazor": {
|
||||
"commandName": "Project",
|
||||
"launchBrowser": true,
|
||||
"environmentVariables": {
|
||||
"ASPNETCORE_ENVIRONMENT": "Development"
|
||||
},
|
||||
"applicationUrl": "https://localhost:49582;http://localhost:49583"
|
||||
}
|
||||
}
|
||||
}
|
||||
10
EnvelopeGenerator.ReceiverUIBlazor/Shared/MainLayout.razor
Normal file
10
EnvelopeGenerator.ReceiverUIBlazor/Shared/MainLayout.razor
Normal 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>
|
||||
8
EnvelopeGenerator.ReceiverUIBlazor/_Imports.razor
Normal file
8
EnvelopeGenerator.ReceiverUIBlazor/_Imports.razor
Normal 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
|
||||
241
EnvelopeGenerator.ReceiverUIBlazor/wwwroot/css/app.css
Normal file
241
EnvelopeGenerator.ReceiverUIBlazor/wwwroot/css/app.css
Normal 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;
|
||||
}
|
||||
19
EnvelopeGenerator.ReceiverUIBlazor/wwwroot/index.html
Normal file
19
EnvelopeGenerator.ReceiverUIBlazor/wwwroot/index.html
Normal 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>
|
||||
339
EnvelopeGenerator.ReceiverUIBlazor/wwwroot/js/pdfInterop.js
Normal file
339
EnvelopeGenerator.ReceiverUIBlazor/wwwroot/js/pdfInterop.js
Normal 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
})();
|
||||
@@ -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>();
|
||||
|
||||
@@ -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" />
|
||||
@@ -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));
|
||||
@@ -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]
|
||||
@@ -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));
|
||||
}
|
||||
}
|
||||
52
EnvelopeGenerator.Web/Client/receiver-ui-react/package.json
Normal file
52
EnvelopeGenerator.Web/Client/receiver-ui-react/package.json
Normal 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 |
@@ -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 |
@@ -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"
|
||||
}
|
||||
10
EnvelopeGenerator.Web/Client/receiver-ui-react/src/App.css
Normal file
10
EnvelopeGenerator.Web/Client/receiver-ui-react/src/App.css
Normal file
@@ -0,0 +1,10 @@
|
||||
body {
|
||||
font-family: "Open Sans";
|
||||
}
|
||||
|
||||
input:focus,
|
||||
select:focus,
|
||||
textarea:focus,
|
||||
button:focus {
|
||||
outline: none;
|
||||
}
|
||||
278
EnvelopeGenerator.Web/Client/receiver-ui-react/src/App.js
Normal file
278
EnvelopeGenerator.Web/Client/receiver-ui-react/src/App.js
Normal 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;
|
||||
@@ -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();
|
||||
});
|
||||
37
EnvelopeGenerator.Web/Client/receiver-ui-react/src/Drop.js
Normal file
37
EnvelopeGenerator.Web/Client/receiver-ui-react/src/Drop.js
Normal 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>
|
||||
);
|
||||
}
|
||||
15
EnvelopeGenerator.Web/Client/receiver-ui-react/src/Header.js
Normal file
15
EnvelopeGenerator.Web/Client/receiver-ui-react/src/Header.js
Normal 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>
|
||||
}
|
||||
@@ -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>
|
||||
}
|
||||
/>
|
||||
);
|
||||
}
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
@@ -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];
|
||||
}
|
||||
@@ -0,0 +1,6 @@
|
||||
import {useWindowSize} from './useWindowSize';
|
||||
|
||||
export function useIsSmallScreen() {
|
||||
const windowSize = useWindowSize();
|
||||
return windowSize.width < 600;
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
13
EnvelopeGenerator.Web/Client/receiver-ui-react/src/index.css
Normal file
13
EnvelopeGenerator.Web/Client/receiver-ui-react/src/index.css
Normal 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;
|
||||
}
|
||||
17
EnvelopeGenerator.Web/Client/receiver-ui-react/src/index.js
Normal file
17
EnvelopeGenerator.Web/Client/receiver-ui-react/src/index.js
Normal 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();
|
||||
@@ -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 |
@@ -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;
|
||||
@@ -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';
|
||||
@@ -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);
|
||||
}
|
||||
@@ -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)';
|
||||
11620
EnvelopeGenerator.Web/Client/receiver-ui-react/yarn.lock
Normal file
11620
EnvelopeGenerator.Web/Client/receiver-ui-react/yarn.lock
Normal file
File diff suppressed because it is too large
Load Diff
@@ -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);
|
||||
|
||||
@@ -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"];
|
||||
|
||||
|
||||
@@ -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>
|
||||
|
||||
|
||||
@@ -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">
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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
Reference in New Issue
Block a user