Compare commits
42 Commits
feat/annot
...
786a3e128d
| Author | SHA1 | Date | |
|---|---|---|---|
| 786a3e128d | |||
| ff3a146636 | |||
| 40b2cad598 | |||
| 5c675be0ed | |||
| 58164be640 | |||
| a639377195 | |||
| e3d6e87ee5 | |||
| 2795b91386 | |||
| ca248c3aa6 | |||
| 383634fca6 | |||
| 75097afa06 | |||
| 77975c0644 | |||
| 5707213edd | |||
| ad54ba9dc4 | |||
| 1f233153cf | |||
| 513ec007eb | |||
| 1305714da2 | |||
| 1e90cda393 | |||
| 5a5cbcb14d | |||
| a35f06070a | |||
| 2606066103 | |||
| 7495e062a9 | |||
| 293044bec3 | |||
| e0ff976d21 | |||
| bec45ab1f1 | |||
| fecd054a5c | |||
| 32b488c50f | |||
| 9cfdd16970 | |||
| 4da5848253 | |||
| 88da210ba2 | |||
| fc23ba840e | |||
| 140d271b28 | |||
| a3b12a6957 | |||
| 16bdc7820d | |||
| 06e32b99ea | |||
| c7c78f96a6 | |||
| 5c232e61f2 | |||
| 24c9321c0f | |||
| c75c2b1dd5 | |||
| 8445757f34 | |||
| b088eb089f | |||
| 1f745ae79c |
@@ -74,6 +74,11 @@ 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; } = null!;
|
||||
public PsPdfKitAnnotation? PsPdfKitAnnotation { get; init; }
|
||||
|
||||
/// <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,6 +29,9 @@ public class AnnotationHandler : INotificationHandler<DocSignedNotification>
|
||||
/// <param name="notification"></param>
|
||||
/// <param name="cancel"></param>
|
||||
/// <returns></returns>
|
||||
public Task Handle(DocSignedNotification notification, CancellationToken cancel)
|
||||
=> _repo.CreateAsync(notification.PsPdfKitAnnotation.Structured, cancel);
|
||||
public async Task Handle(DocSignedNotification notification, CancellationToken cancel)
|
||||
{
|
||||
if (notification.PsPdfKitAnnotation is PsPdfKitAnnotation annot)
|
||||
await _repo.CreateAsync(annot.Structured, cancel);
|
||||
}
|
||||
}
|
||||
@@ -10,6 +10,8 @@ namespace EnvelopeGenerator.Application.Common.Notifications.DocSigned.Handlers;
|
||||
/// </summary>
|
||||
public class DocStatusHandler : INotificationHandler<DocSignedNotification>
|
||||
{
|
||||
private const string BlankAnnotationJson = "{}";
|
||||
|
||||
private readonly ISender _sender;
|
||||
|
||||
/// <summary>
|
||||
@@ -33,7 +35,9 @@ public class DocStatusHandler : INotificationHandler<DocSignedNotification>
|
||||
{
|
||||
Envelope = new() { Id = notification.EnvelopeId },
|
||||
Receiver = new() { Id = notification.ReceiverId},
|
||||
Value = JsonSerializer.Serialize(notification.PsPdfKitAnnotation.Instant, Format.Json.ForAnnotations)
|
||||
Value = notification.PsPdfKitAnnotation is PsPdfKitAnnotation annot
|
||||
? JsonSerializer.Serialize(annot.Instant, Format.Json.ForAnnotations)
|
||||
: BlankAnnotationJson
|
||||
}, cancel);
|
||||
}
|
||||
}
|
||||
@@ -14,7 +14,7 @@
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Dapper" Version="2.1.66" />
|
||||
<PackageReference Include="DigitalData.Core.Abstraction.Application" Version="1.4.0" />
|
||||
<PackageReference Include="DigitalData.Core.Abstraction.Application" Version="1.6.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.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 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>
|
||||
<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>
|
||||
|
||||
@@ -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.4.0" targetFramework="net462" />
|
||||
<package id="DigitalData.Core.Abstraction.Application" version="1.6.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.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 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>
|
||||
<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>
|
||||
|
||||
@@ -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.4.0" targetFramework="net462" />
|
||||
<package id="DigitalData.Core.Abstraction.Application" version="1.6.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,6 +2,7 @@
|
||||
using System.ComponentModel.DataAnnotations;
|
||||
using System.ComponentModel.DataAnnotations.Schema;
|
||||
using EnvelopeGenerator.Domain.Constants;
|
||||
using Newtonsoft.Json;
|
||||
|
||||
#if NETFRAMEWORK
|
||||
using System;
|
||||
|
||||
@@ -12,7 +12,7 @@ namespace EnvelopeGenerator.Infrastructure
|
||||
public EGDbContext CreateDbContext(string[] args)
|
||||
{
|
||||
var config = new ConfigurationBuilder()
|
||||
.SetBasePath(Directory.GetCurrentDirectory()) // Önemli!
|
||||
.SetBasePath(Directory.GetCurrentDirectory())
|
||||
.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.4.0" />
|
||||
<PackageReference Include="DigitalData.Core.Infrastructure" Version="2.4.5" />
|
||||
<PackageReference Include="DigitalData.Core.Abstraction.Application" Version="1.6.0" />
|
||||
<PackageReference Include="DigitalData.Core.Infrastructure" Version="2.6.1" />
|
||||
<PackageReference Include="QuestPDF" Version="2025.7.1" />
|
||||
</ItemGroup>
|
||||
|
||||
|
||||
7
EnvelopeGenerator.Jobs/Class1.cs
Normal file
7
EnvelopeGenerator.Jobs/Class1.cs
Normal file
@@ -0,0 +1,7 @@
|
||||
namespace EnvelopeGenerator.Jobs
|
||||
{
|
||||
public class Class1
|
||||
{
|
||||
|
||||
}
|
||||
}
|
||||
41
EnvelopeGenerator.Jobs/EnvelopeGenerator.Jobs.csproj
Normal file
41
EnvelopeGenerator.Jobs/EnvelopeGenerator.Jobs.csproj
Normal file
@@ -0,0 +1,41 @@
|
||||
<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>
|
||||
|
||||
<ItemGroup>
|
||||
<Content Include="Jobs\**\*.cs">
|
||||
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
|
||||
</Content>
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<Compile Remove="Jobs\APIBackendJobs\APIEnvelopeJob.cs" />
|
||||
<Compile Remove="Jobs\DataRowExtensions.cs" />
|
||||
<Compile Remove="Jobs\FinalizeDocument\FinalizeDocumentExceptions.cs" />
|
||||
<Compile Remove="Jobs\FinalizeDocument\FinalizeDocumentJob.cs" />
|
||||
<Compile Remove="Jobs\FinalizeDocument\PDFBurner.cs" />
|
||||
<Compile Remove="Jobs\FinalizeDocument\PDFBurnerParams.cs" />
|
||||
<Compile Remove="Jobs\FinalizeDocument\PDFMerger.cs" />
|
||||
<Compile Remove="Jobs\FinalizeDocument\ReportCreator.cs" />
|
||||
<Compile Remove="Jobs\FinalizeDocument\ReportItem.cs" />
|
||||
<Compile Remove="Jobs\FinalizeDocument\ReportSource.cs" />
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
160
EnvelopeGenerator.Jobs/Jobs/APIBackendJobs/APIEnvelopeJob.cs
Normal file
160
EnvelopeGenerator.Jobs/Jobs/APIBackendJobs/APIEnvelopeJob.cs
Normal file
@@ -0,0 +1,160 @@
|
||||
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.CommonServices.Jobs.APIBackendJobs;
|
||||
|
||||
public class APIEnvelopeJob : IJob
|
||||
{
|
||||
private readonly ILogger<APIEnvelopeJob> _logger;
|
||||
|
||||
public APIEnvelopeJob() : this(NullLogger<APIEnvelopeJob>.Instance)
|
||||
{
|
||||
}
|
||||
|
||||
public APIEnvelopeJob(ILogger<APIEnvelopeJob> logger)
|
||||
{
|
||||
_logger = logger;
|
||||
}
|
||||
|
||||
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");
|
||||
}
|
||||
}
|
||||
}
|
||||
30
EnvelopeGenerator.Jobs/Jobs/DataRowExtensions.cs
Normal file
30
EnvelopeGenerator.Jobs/Jobs/DataRowExtensions.cs
Normal file
@@ -0,0 +1,30 @@
|
||||
using System;
|
||||
using System.Data;
|
||||
|
||||
namespace EnvelopeGenerator.CommonServices.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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,28 @@
|
||||
namespace EnvelopeGenerator.CommonServices.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) { }
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,229 @@
|
||||
using System.Collections.Generic;
|
||||
using System.Data;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using EnvelopeGenerator.CommonServices.Jobs.FinalizeDocument.FinalizeDocumentExceptions;
|
||||
using EnvelopeGenerator.Domain.Constants;
|
||||
using EnvelopeGenerator.Domain.Entities;
|
||||
using Microsoft.Data.SqlClient;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Microsoft.Extensions.Logging.Abstractions;
|
||||
using Quartz;
|
||||
|
||||
namespace EnvelopeGenerator.CommonServices.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()
|
||||
: this(NullLogger<FinalizeDocumentJob>.Instance)
|
||||
{
|
||||
}
|
||||
|
||||
public FinalizeDocumentJob(ILogger<FinalizeDocumentJob> logger)
|
||||
{
|
||||
_logger = logger;
|
||||
_pdfBurner = new PDFBurner();
|
||||
_pdfMerger = new PDFMerger();
|
||||
_reportCreator = new 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 envelope = new Envelope
|
||||
{
|
||||
Id = envelopeId,
|
||||
Uuid = envelopeData.EnvelopeUuid ?? string.Empty,
|
||||
Title = envelopeData.Title ?? string.Empty,
|
||||
FinalEmailToCreator = FinalEmailType.No,
|
||||
FinalEmailToReceivers = FinalEmailType.No
|
||||
};
|
||||
|
||||
var burned = _pdfBurner.BurnAnnotsToPDF(envelopeData.DocumentBytes, envelopeData.AnnotationData, envelopeId);
|
||||
var report = _reportCreator.CreateReport(connection, envelope);
|
||||
var merged = _pdfMerger.MergeDocuments(burned, report);
|
||||
|
||||
var outputDirectory = Path.Combine(config.ExportPath, envelopeData.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);
|
||||
}
|
||||
}
|
||||
248
EnvelopeGenerator.Jobs/Jobs/FinalizeDocument/PDFBurner.cs
Normal file
248
EnvelopeGenerator.Jobs/Jobs/FinalizeDocument/PDFBurner.cs
Normal file
@@ -0,0 +1,248 @@
|
||||
using System.Collections.Generic;
|
||||
using System.Drawing;
|
||||
using System.Linq;
|
||||
using EnvelopeGenerator.CommonServices.Jobs.FinalizeDocument.FinalizeDocumentExceptions;
|
||||
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 Microsoft.Extensions.Logging;
|
||||
using Microsoft.Extensions.Logging.Abstractions;
|
||||
using Newtonsoft.Json;
|
||||
|
||||
namespace EnvelopeGenerator.CommonServices.Jobs.FinalizeDocument;
|
||||
|
||||
public class PDFBurner
|
||||
{
|
||||
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 canvas = new PdfCanvas(page);
|
||||
var bounds = annotation.bbox.Select(ToInches).ToList();
|
||||
var x = bounds[0];
|
||||
var y = bounds[1];
|
||||
var width = bounds[2];
|
||||
var height = bounds[3];
|
||||
|
||||
var imageBytes = Convert.FromBase64String(attachment.binary);
|
||||
var imageData = ImageDataFactory.Create(imageBytes);
|
||||
canvas.AddImageAt(imageData, x, y, false)
|
||||
.ScaleToFit(width, height);
|
||||
}
|
||||
|
||||
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 void AddFormFieldValue(PdfDocument pdf, Annotation annotation, string value)
|
||||
{
|
||||
var bounds = annotation.bbox.Select(ToInches).ToList();
|
||||
var x = bounds[0];
|
||||
var y = bounds[1];
|
||||
var width = bounds[2];
|
||||
var height = bounds[3];
|
||||
|
||||
var page = pdf.GetPage(annotation.pageIndex + 1);
|
||||
var canvas = new Canvas(new PdfCanvas(page), page.GetPageSize());
|
||||
canvas.ShowTextAligned(new Paragraph(value)
|
||||
.SetFontSize(_pdfBurnerParams.FontSize)
|
||||
.SetFontColor(ColorConstants.BLACK)
|
||||
.SetFontFamily(_pdfBurnerParams.FontName)
|
||||
.SetItalic(_pdfBurnerParams.FontStyle.HasFlag(FontStyle.Italic))
|
||||
.SetBold(_pdfBurnerParams.FontStyle.HasFlag(FontStyle.Bold)),
|
||||
x + _pdfBurnerParams.TopMargin,
|
||||
y + _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
|
||||
}
|
||||
@@ -0,0 +1,19 @@
|
||||
using System.Collections.Generic;
|
||||
using System.Drawing;
|
||||
|
||||
namespace EnvelopeGenerator.CommonServices.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;
|
||||
}
|
||||
46
EnvelopeGenerator.Jobs/Jobs/FinalizeDocument/PDFMerger.cs
Normal file
46
EnvelopeGenerator.Jobs/Jobs/FinalizeDocument/PDFMerger.cs
Normal file
@@ -0,0 +1,46 @@
|
||||
using System.IO;
|
||||
using EnvelopeGenerator.CommonServices.Jobs.FinalizeDocument.FinalizeDocumentExceptions;
|
||||
using iText.Kernel.Pdf;
|
||||
using iText.Kernel.Utils;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Microsoft.Extensions.Logging.Abstractions;
|
||||
|
||||
namespace EnvelopeGenerator.CommonServices.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);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,93 @@
|
||||
using System.Data;
|
||||
using System.IO;
|
||||
using EnvelopeGenerator.CommonServices.Jobs.FinalizeDocument.FinalizeDocumentExceptions;
|
||||
using EnvelopeGenerator.Domain.Entities;
|
||||
using iText.Kernel.Pdf;
|
||||
using iText.Layout;
|
||||
using iText.Layout.Element;
|
||||
using Microsoft.Data.SqlClient;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Microsoft.Extensions.Logging.Abstractions;
|
||||
|
||||
namespace EnvelopeGenerator.CommonServices.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 Document(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, HEAD_TITLE, HEAD_SUBJECT, 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),
|
||||
EnvelopeTitle = reader.IsDBNull(1) ? string.Empty : reader.GetString(1),
|
||||
EnvelopeSubject = reader.IsDBNull(2) ? string.Empty : reader.GetString(2),
|
||||
ItemDate = reader.IsDBNull(3) ? DateTime.MinValue : reader.GetDateTime(3),
|
||||
ItemStatus = reader.IsDBNull(4) ? default : (EnvelopeGenerator.Domain.Constants.EnvelopeStatus)reader.GetInt32(4),
|
||||
ItemUserReference = reader.IsDBNull(5) ? string.Empty : reader.GetString(5)
|
||||
});
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
}
|
||||
19
EnvelopeGenerator.Jobs/Jobs/FinalizeDocument/ReportItem.cs
Normal file
19
EnvelopeGenerator.Jobs/Jobs/FinalizeDocument/ReportItem.cs
Normal file
@@ -0,0 +1,19 @@
|
||||
using EnvelopeGenerator.Domain.Constants;
|
||||
using EnvelopeGenerator.Domain.Entities;
|
||||
|
||||
namespace EnvelopeGenerator.CommonServices.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; }
|
||||
}
|
||||
@@ -0,0 +1,8 @@
|
||||
using System.Collections.Generic;
|
||||
|
||||
namespace EnvelopeGenerator.CommonServices.Jobs.FinalizeDocument;
|
||||
|
||||
public class ReportSource
|
||||
{
|
||||
public List<ReportItem> Items { get; set; } = new();
|
||||
}
|
||||
@@ -5,6 +5,7 @@ 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;
|
||||
@@ -44,9 +45,15 @@ public class DocSignedNotificationTests : TestBase
|
||||
|
||||
// Create envelope receiver
|
||||
var envRcv = this.CreateEnvelopeReceiver(env!.Id, rcv.Id);
|
||||
envRcv = await Repository.CreateAsync(envRcv, cancel);
|
||||
|
||||
var repo = GetRepository<EnvelopeReceiver>();
|
||||
|
||||
envRcv = await repo.CreateAsync(envRcv, cancel);
|
||||
var envRcvDto = _mapper.Map<EnvelopeReceiverDto>(envRcv);
|
||||
var docSignedNtf = envRcvDto.ToDocSignedNotification(new () { });
|
||||
|
||||
var annots = Services.GetRequiredService<PsPdfKitAnnotation>();
|
||||
|
||||
var docSignedNtf = envRcvDto.ToDocSignedNotification(annots);
|
||||
|
||||
var sendSignedMailHandler = Host.Services.GetRequiredService<SendSignedMailHandler>();
|
||||
|
||||
@@ -1,8 +1,10 @@
|
||||
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;
|
||||
@@ -11,6 +13,7 @@ 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;
|
||||
@@ -20,7 +23,6 @@ using Microsoft.Extensions.Logging;
|
||||
using Microsoft.Extensions.Options;
|
||||
using QuestPDF.Fluent;
|
||||
using QuestPDF.Infrastructure;
|
||||
using EnvelopeGenerator.Application.Common.Extensions;
|
||||
|
||||
namespace EnvelopeGenerator.Tests.Application;
|
||||
|
||||
@@ -42,10 +44,29 @@ public class Fake
|
||||
// add Application and Infrastructure services
|
||||
#pragma warning disable CS0618
|
||||
services.AddEnvelopeGeneratorServices(configuration);
|
||||
services.AddEnvelopeGeneratorInfrastructureServices(
|
||||
(sp, options) => options.UseInMemoryDatabase("EnvelopeGeneratorTestDb"),
|
||||
context.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);
|
||||
|
||||
});
|
||||
|
||||
var prodCnnStr = context.Configuration.GetConnectionString("Default");
|
||||
services.AddDbContext<EGDbContext2Prod>(opt => opt.UseSqlServer(prodCnnStr));
|
||||
@@ -24,6 +24,8 @@ public abstract class TestBase : Faker
|
||||
|
||||
protected IRepository Repository => Host.Services.GetRequiredService<IRepository>();
|
||||
|
||||
protected IServiceProvider Services => Host.Services;
|
||||
|
||||
protected abstract void ConfigureServices(IServiceCollection services);
|
||||
|
||||
[SetUp]
|
||||
@@ -32,9 +34,11 @@ 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 Repository.CreateAsync(temp);
|
||||
await repo.CreateAsync(temp);
|
||||
}
|
||||
|
||||
[TearDown]
|
||||
17
EnvelopeGenerator.Tests/Domain/ConstantsTests.cs
Normal file
17
EnvelopeGenerator.Tests/Domain/ConstantsTests.cs
Normal file
@@ -0,0 +1,17 @@
|
||||
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));
|
||||
}
|
||||
}
|
||||
@@ -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.4.0" />
|
||||
<PackageReference Include="DigitalData.Core.Abstraction.Application" Version="1.6.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" />
|
||||
@@ -44,7 +44,7 @@ public class AnnotationController : ControllerBase
|
||||
|
||||
[Authorize(Roles = ReceiverRole.FullyAuth)]
|
||||
[HttpPost]
|
||||
public async Task<IActionResult> CreateOrUpdate([FromBody] PsPdfKitAnnotation psPdfKitAnnotation, CancellationToken cancel = default)
|
||||
public async Task<IActionResult> CreateOrUpdate([FromBody] PsPdfKitAnnotation? psPdfKitAnnotation = null, CancellationToken cancel = default)
|
||||
{
|
||||
// get claims
|
||||
var signature = User.GetAuthReceiverSignature();
|
||||
@@ -56,6 +56,12 @@ 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,7 +248,9 @@ public class EnvelopeController : ViewControllerBase
|
||||
{
|
||||
if (er.Envelope!.Documents?.FirstOrDefault() is DocumentDto doc && doc.ByteData is not null)
|
||||
{
|
||||
using var pdf = Pdf.FromMemory(doc.ByteData).Background(doc.Elements!);
|
||||
using var pdf = er.Envelope.ReadOnly
|
||||
? Pdf.FromMemory(doc.ByteData)
|
||||
: Pdf.FromMemory(doc.ByteData).Background(doc.Elements!);
|
||||
|
||||
doc.ByteData = pdf.ExportAsBytes();
|
||||
|
||||
@@ -262,6 +264,8 @@ public class EnvelopeController : ViewControllerBase
|
||||
|
||||
await HttpContext.SignInEnvelopeAsync(er, ReceiverRole.FullyAuth);
|
||||
|
||||
ViewData["ReadAndConfirm"] = er.Envelope.ReadOnly;
|
||||
|
||||
//add PSPDFKit licence key
|
||||
ViewData["PSPDFKitLicenseKey"] = _configuration["PSPDFKitLicenseKey"];
|
||||
|
||||
|
||||
49
EnvelopeGenerator.Web/EnvelopeCookieManager.cs
Normal file
49
EnvelopeGenerator.Web/EnvelopeCookieManager.cs
Normal file
@@ -0,0 +1,49 @@
|
||||
using Microsoft.AspNetCore.Authentication.Cookies;
|
||||
|
||||
namespace EnvelopeGenerator.Web;
|
||||
|
||||
public class EnvelopeCookieManager : ICookieManager
|
||||
{
|
||||
private readonly IEnumerable<string> _envelopeKeyBasedCookieNames;
|
||||
|
||||
private readonly ChunkingCookieManager _inner = new();
|
||||
|
||||
public EnvelopeCookieManager(params string[] envelopeKeyBasedCookieNames)
|
||||
{
|
||||
_envelopeKeyBasedCookieNames = envelopeKeyBasedCookieNames;
|
||||
}
|
||||
|
||||
private string GetCookieName(HttpContext context, string key)
|
||||
{
|
||||
if (!_envelopeKeyBasedCookieNames.Contains(key))
|
||||
return key;
|
||||
|
||||
var envId = context.GetRouteValue("envelopeReceiverId")?.ToString();
|
||||
|
||||
if (string.IsNullOrEmpty(envId) && context.Request.Query.TryGetValue("envKey", out var envKeyValue))
|
||||
envId = envKeyValue;
|
||||
|
||||
if (string.IsNullOrEmpty(envId))
|
||||
return key;
|
||||
|
||||
return $"{key}-{envId}";
|
||||
}
|
||||
|
||||
public string? GetRequestCookie(HttpContext context, string key)
|
||||
{
|
||||
var cookieName = GetCookieName(context, key);
|
||||
return _inner.GetRequestCookie(context, cookieName);
|
||||
}
|
||||
|
||||
public void AppendResponseCookie(HttpContext context, string key, string? value, CookieOptions options)
|
||||
{
|
||||
var cookieName = GetCookieName(context, key);
|
||||
_inner.AppendResponseCookie(context, cookieName, value, options);
|
||||
}
|
||||
|
||||
public void DeleteCookie(HttpContext context, string key, CookieOptions options)
|
||||
{
|
||||
var cookieName = GetCookieName(context, key);
|
||||
_inner.DeleteCookie(context, cookieName, options);
|
||||
}
|
||||
}
|
||||
@@ -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.7.0</Version>
|
||||
<AssemblyVersion>3.7.0</AssemblyVersion>
|
||||
<FileVersion>3.7.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>
|
||||
|
||||
|
||||
@@ -17,6 +17,7 @@ using EnvelopeGenerator.Web.Models.Annotation;
|
||||
using DigitalData.UserManager.DependencyInjection;
|
||||
using EnvelopeGenerator.Web.Middleware;
|
||||
using EnvelopeGenerator.Application.Common.Interfaces.Services;
|
||||
using EnvelopeGenerator.Web;
|
||||
|
||||
var logger = LogManager.Setup().LoadConfigurationFromAppSettings().GetCurrentClassLogger();
|
||||
logger.Info("Logging initialized!");
|
||||
@@ -134,41 +135,22 @@ try
|
||||
options.ConsentCookie.Name = "cookie-consent-settings";
|
||||
});
|
||||
|
||||
var authCookieName = "env_auth";
|
||||
builder.Services.AddAuthentication(CookieAuthenticationDefaults.AuthenticationScheme)
|
||||
.AddCookie(options =>
|
||||
{
|
||||
options.Cookie.HttpOnly = true; // Makes the cookie inaccessible to client-side scripts for security
|
||||
options.Cookie.SecurePolicy = CookieSecurePolicy.SameAsRequest; // Ensures cookies are sent over HTTPS only
|
||||
options.Cookie.SameSite = SameSiteMode.Strict; // Protects against CSRF attacks by restricting how cookies are sent with requests from external sites
|
||||
options.Cookie.Name = authCookieName;
|
||||
options.CookieManager = new EnvelopeCookieManager(authCookieName);
|
||||
options.Cookie.HttpOnly = true;
|
||||
options.Cookie.SecurePolicy = CookieSecurePolicy.SameAsRequest;
|
||||
options.Cookie.SameSite = SameSiteMode.Strict;
|
||||
options.ExpireTimeSpan = TimeSpan.FromMinutes(30);
|
||||
|
||||
options.Events = new CookieAuthenticationEvents
|
||||
{
|
||||
OnRedirectToLogin = context =>
|
||||
{
|
||||
// Dynamically calculate the redirection path, for example:
|
||||
var envelopeReceiverId = context.HttpContext.Request.RouteValues["envelopeReceiverId"];
|
||||
context.RedirectUri = $"/EnvelopeKey/{envelopeReceiverId}";
|
||||
|
||||
context.Response.Redirect(context.RedirectUri);
|
||||
return Task.CompletedTask;
|
||||
},
|
||||
OnRedirectToLogout = context =>
|
||||
{
|
||||
// Apply a similar redirection logic for logout
|
||||
var envelopeReceiverId = context.HttpContext.Request.RouteValues["envelopeReceiverId"];
|
||||
context.RedirectUri = $"/EnvelopeKey/{envelopeReceiverId}";
|
||||
|
||||
context.Response.Redirect(context.RedirectUri);
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
};
|
||||
});
|
||||
|
||||
builder.Services.AddSingleton(config.GetSection("ContactLink").Get<ContactLink>() ?? new());
|
||||
|
||||
builder.Services.AddCookieBasedLocalizer();
|
||||
|
||||
|
||||
builder.Services.AddSingleton(HtmlEncoder.Default);
|
||||
builder.Services.AddSingleton(UrlEncoder.Default);
|
||||
builder.Services.AddSanitizer<HtmlSanitizer>();
|
||||
@@ -249,7 +231,7 @@ try
|
||||
app.UseAuthorization();
|
||||
|
||||
var cultures = app.Services.GetRequiredService<Cultures>();
|
||||
if(!cultures.Any())
|
||||
if (!cultures.Any())
|
||||
throw new InvalidOperationException(@"Languages section is missing in the appsettings. Please configure like following.
|
||||
Language is both a name of the culture and the name of the resx file such as Resource.de-DE.resx
|
||||
FIClass is the css class (in wwwroot/lib/flag-icons-main) for the flag of country.
|
||||
@@ -264,7 +246,7 @@ try
|
||||
}
|
||||
]");
|
||||
|
||||
if(!config.GetValue<bool>("DisableMultiLanguage"))
|
||||
if (!config.GetValue<bool>("DisableMultiLanguage"))
|
||||
app.UseCookieBasedLocalizer(cultures.Languages.ToArray());
|
||||
|
||||
app.UseCors("SameOriginPolicy");
|
||||
@@ -273,7 +255,7 @@ try
|
||||
app.MapFallbackToController("Error404", "Home");
|
||||
app.Run();
|
||||
}
|
||||
catch(Exception ex)
|
||||
catch (Exception ex)
|
||||
{
|
||||
logger.Error(ex, "Stopped program because of exception");
|
||||
throw;
|
||||
|
||||
@@ -43,12 +43,14 @@
|
||||
</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,6 +42,8 @@
|
||||
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,15 +35,18 @@ class App {
|
||||
});
|
||||
|
||||
// Load annotations into PSPDFKit
|
||||
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);
|
||||
}
|
||||
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);
|
||||
}
|
||||
|
||||
//add click events of external buttons
|
||||
[...document.getElementsByClassName('btn_refresh')].forEach(btn => btn.addEventListener('click', _ => this.handleClick('RESET')));
|
||||
if (!READ_AND_CONFIRM) {
|
||||
[...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')));
|
||||
}
|
||||
@@ -182,45 +185,70 @@ class App {
|
||||
}
|
||||
|
||||
async handleFinish(event) {
|
||||
const iJSON = await this.pdfKit.exportInstantJSON()
|
||||
|
||||
const iFormFieldValues = iJSON.formFieldValues;
|
||||
let annotResult = undefined;
|
||||
|
||||
//check required
|
||||
const iReqFields = iFormFieldValues.filter(f => isFieldRequired(f))
|
||||
const hasEmptyReq = iReqFields.some(f => (f.value === undefined || f.value === null || f.value === ""))
|
||||
// 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.'
|
||||
|
||||
if (hasEmptyReq) {
|
||||
Swal.fire({
|
||||
title: 'Warnung',
|
||||
text: 'Bitte füllen Sie alle Standortinformationen vollständig aus!',
|
||||
icon: 'warning',
|
||||
})
|
||||
return false;
|
||||
await Swal.fire({
|
||||
title: 'Warnung',
|
||||
text: message,
|
||||
icon: 'warning'
|
||||
})
|
||||
return false
|
||||
}
|
||||
}
|
||||
else {
|
||||
const iJSON = await this.pdfKit.exportInstantJSON()
|
||||
|
||||
//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)) {
|
||||
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) {
|
||||
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.`,
|
||||
text: 'Bitte füllen Sie alle Standortinformationen vollständig aus!',
|
||||
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
|
||||
//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) };
|
||||
}
|
||||
|
||||
return Swal.fire({
|
||||
@@ -249,10 +277,7 @@ class App {
|
||||
|
||||
// Export annotation data and save to database
|
||||
try {
|
||||
const res = await signEnvelope({
|
||||
instant: iJSON,
|
||||
structured: mapSignature(iJSON)
|
||||
});
|
||||
const res = READ_AND_CONFIRM ? await signEnvelope() : await signEnvelope(annotResult);
|
||||
|
||||
if (!res.ok) {
|
||||
if (res.status === 409) {
|
||||
|
||||
4
EnvelopeGenerator.Web/wwwroot/js/app.min.js
vendored
4
EnvelopeGenerator.Web/wwwroot/js/app.min.js
vendored
@@ -1,3 +1,3 @@
|
||||
class App{constructor(n,t,i,r,u,f){this.container=f??`#${this.constructor.name.toLowerCase()}`;this.envelopeKey=n;this.pdfKit=null;this.currentDocument=t.envelope.documents[0];this.currentReceiver=t.receiver;this.signatureCount=t.envelope.documents[0].elements.length;this.envelopeReceiver=t;this.documentBytes=i;this.licenseKey=r;this.locale=u}async init(){this.pdfKit=await loadPSPDFKit(this.documentBytes,this.container,this.licenseKey,this.locale);addToolbarItems(this.pdfKit,this.handleClick.bind(this));this.pdfKit.addEventListener("annotations.load",this.handleAnnotationsLoad.bind(this));this.pdfKit.addEventListener("annotations.change",this.handleAnnotationsChange.bind(this));this.pdfKit.addEventListener("annotations.create",this.handleAnnotationsCreate.bind(this));this.pdfKit.addEventListener("annotations.willChange",()=>{Comp.ActPanel.Toggle()});try{let n=await createAnnotations(this.currentDocument,this.envelopeReceiver.envelopeId,this.envelopeReceiver.receiverId);await this.pdfKit.create(n)}catch(n){console.error("Error loading annotations:",n)}[...document.getElementsByClassName("btn_refresh")].forEach(n=>n.addEventListener("click",()=>this.handleClick("RESET")));[...document.getElementsByClassName("btn_complete")].forEach(n=>n.addEventListener("click",()=>this.handleClick("FINISH")));[...document.getElementsByClassName("btn_reject")].forEach(n=>n.addEventListener("click",()=>this.handleClick("REJECT")))}handleAnnotationsLoad(n){n.toJS()}handleAnnotationsChange(){}async handleAnnotationsCreate(n){const t=n.toJS()[0],i=!!t.formFieldName,r=!!t.isSignature;if(i===!1&&r===!0){const r=t.boundingBox.left-20,u=t.boundingBox.top-20,n=150,i=75,f=new Date,e=await createAnnotationFrameBlob(this.envelopeReceiver.name,this.currentReceiver.signature,f,n,i),o=await fetch(e),s=await o.blob(),h=await this.pdfKit.createAttachment(s),c=createImageAnnotation(new PSPDFKit.Geometry.Rect({left:r,top:u,width:n,height:i}),t.pageIndex,h,generateId(this.envelopeReceiver.envelopeId,this.envelopeReceiver.receiverId,this.fakeElementId--,"signed"));this.pdfKit.create(c)}}async handleClick(n){let t=!1;switch(n){case"RESET":t=await this.handleReset(null);Comp.SignatureProgress.SignedCount=0;t.isConfirmed&&Swal.fire({title:"Erfolg",text:"Dokument wurde zurückgesetzt",icon:"info"});break;case"FINISH":t=await this.handleFinish(null);t==!0&&(window.location.href=`/Envelope/${this.envelopeKey}`);break;case"REJECT":Swal.fire({title:localized.rejection,html:`<div class="text-start fs-6 p-0 m-0">${localized.rejectionReasonQ}</div>`,icon:"question",input:"text",inputAttributes:{autocapitalize:"off"},showCancelButton:!0,confirmButtonColor:"#3085d6",cancelButtonColor:"#d33",confirmButtonText:localized.complete,cancelButtonText:localized.back,showLoaderOnConfirm:!0,preConfirm:async n=>{try{return await rejectEnvelope(n)}catch(t){Swal.showValidationMessage(`
|
||||
class App{constructor(n,t,i,r,u,f){this.container=f??`#${this.constructor.name.toLowerCase()}`;this.envelopeKey=n;this.pdfKit=null;this.currentDocument=t.envelope.documents[0];this.currentReceiver=t.receiver;this.signatureCount=t.envelope.documents[0].elements.length;this.envelopeReceiver=t;this.documentBytes=i;this.licenseKey=r;this.locale=u}async init(){if(this.pdfKit=await loadPSPDFKit(this.documentBytes,this.container,this.licenseKey,this.locale),addToolbarItems(this.pdfKit,this.handleClick.bind(this)),this.pdfKit.addEventListener("annotations.load",this.handleAnnotationsLoad.bind(this)),this.pdfKit.addEventListener("annotations.change",this.handleAnnotationsChange.bind(this)),this.pdfKit.addEventListener("annotations.create",this.handleAnnotationsCreate.bind(this)),this.pdfKit.addEventListener("annotations.willChange",()=>{Comp.ActPanel.Toggle()}),!READ_AND_CONFIRM)try{let n=await createAnnotations(this.currentDocument,this.envelopeReceiver.envelopeId,this.envelopeReceiver.receiverId);await this.pdfKit.create(n)}catch(n){console.error("Error loading annotations:",n)}READ_AND_CONFIRM||[...document.getElementsByClassName("btn_refresh")].forEach(n=>n.addEventListener("click",()=>this.handleClick("RESET")));[...document.getElementsByClassName("btn_complete")].forEach(n=>n.addEventListener("click",()=>this.handleClick("FINISH")));[...document.getElementsByClassName("btn_reject")].forEach(n=>n.addEventListener("click",()=>this.handleClick("REJECT")))}handleAnnotationsLoad(n){n.toJS()}handleAnnotationsChange(){}async handleAnnotationsCreate(n){const t=n.toJS()[0],i=!!t.formFieldName,r=!!t.isSignature;if(i===!1&&r===!0){const r=t.boundingBox.left-20,u=t.boundingBox.top-20,n=150,i=75,f=new Date,e=await createAnnotationFrameBlob(this.envelopeReceiver.name,this.currentReceiver.signature,f,n,i),o=await fetch(e),s=await o.blob(),h=await this.pdfKit.createAttachment(s),c=createImageAnnotation(new PSPDFKit.Geometry.Rect({left:r,top:u,width:n,height:i}),t.pageIndex,h,generateId(this.envelopeReceiver.envelopeId,this.envelopeReceiver.receiverId,this.fakeElementId--,"signed"));this.pdfKit.create(c)}}async handleClick(n){let t=!1;switch(n){case"RESET":t=await this.handleReset(null);Comp.SignatureProgress.SignedCount=0;t.isConfirmed&&Swal.fire({title:"Erfolg",text:"Dokument wurde zurückgesetzt",icon:"info"});break;case"FINISH":t=await this.handleFinish(null);t==!0&&(window.location.href=`/Envelope/${this.envelopeKey}`);break;case"REJECT":Swal.fire({title:localized.rejection,html:`<div class="text-start fs-6 p-0 m-0">${localized.rejectionReasonQ}</div>`,icon:"question",input:"text",inputAttributes:{autocapitalize:"off"},showCancelButton:!0,confirmButtonColor:"#3085d6",cancelButtonColor:"#d33",confirmButtonText:localized.complete,cancelButtonText:localized.back,showLoaderOnConfirm:!0,preConfirm:async n=>{try{return await rejectEnvelope(n)}catch(t){Swal.showValidationMessage(`
|
||||
Request failed: ${t}
|
||||
`)}},allowOutsideClick:()=>!Swal.isLoading()}).then(n=>{if(n.isConfirmed){const t=n.value;t.ok?reload():Swal.showValidationMessage(`Request failed: ${t.message}`)}});break;case"COPY_URL":const n=window.location.href.replace(/\/readonly/gi,"");navigator.clipboard.writeText(n).then(function(){bsNotify("Kopiert",{alert_type:"success",delay:4,icon_name:"check_circle"})}).catch(function(){bsNotify("Unerwarteter Fehler",{alert_type:"danger",delay:4,icon_name:"error"})});break;case"SHARE":Comp.ShareBackdrop.show();break;case"LOGOUT":await logout()}}async handleFinish(){const n=await this.pdfKit.exportInstantJSON(),t=n.formFieldValues,r=t.filter(n=>isFieldRequired(n)),u=r.some(n=>n.value===undefined||n.value===null||n.value==="");if(u)return Swal.fire({title:"Warnung",text:"Bitte füllen Sie alle Standortinformationen vollständig aus!",icon:"warning"}),!1;const f=new RegExp("^[a-zA-Z\\u0080-\\u024F]+(?:([\\ \\-\\']|(\\.\\ ))[a-zA-Z\\u0080-\\u024F]+)*$"),e=t.filter(n=>isCityField(n));for(var i of e)if(!IS_MOBILE_DEVICE&&!f.test(i.value))return Swal.fire({title:"Warnung",text:`Bitte überprüfen Sie die eingegebene Ortsangabe "${i.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"}),!1;const o=await this.validateAnnotations(this.signatureCount);return o===!1?(Swal.fire({title:"Warnung",text:"Es wurden nicht alle Signaturfelder ausgefüllt!",icon:"warning"}),!1):Swal.fire({title:localized.confirmation,html:`<div class="text-start fs-6 p-0 m-0">${localized.sigAgree}</div>`,icon:"question",showCancelButton:!0,confirmButtonColor:"#3085d6",cancelButtonColor:"#d33",confirmButtonText:localized.finalize,cancelButtonText:localized.back}).then(async t=>{if(t.isConfirmed){try{await this.pdfKit.save()}catch(i){return Swal.fire({title:"Fehler",text:"Umschlag konnte nicht signiert werden!",icon:"error"}),!1}try{const t=await signEnvelope({instant:n,structured:mapSignature(n)});if(t.ok)return!0;if(t.status===409)return Swal.fire({title:"Warnung",text:"Umschlag ist nicht mehr verfügbar.",icon:"warning"}),!1;if(t.status===423)Swal.fire({title:"Info",text:"Dokument wurde von einem Empfänger abgelehnt. Sie werden weitergeleitet...",icon:"info",timer:2e3,showConfirmButton:!1}).then(()=>{location.reload()});else throw new Error;}catch(i){return Swal.fire({title:"Fehler",text:"Umschlag konnte nicht signiert werden!",icon:"error"}),!1}}else return!1})}async validateAnnotations(n){const t=await getAnnotations(this.pdfKit),i=t.map(n=>n.toJS()).filter(n=>n.isSignature);return n<=i.length}async handleReset(){const n=Swal.fire({title:"Sind sie sicher?",text:"Wollen Sie das Dokument und alle erstellten Signaturen zurücksetzen?",icon:"question",showCancelButton:!0});if(n.isConfirmed){const n=await deleteAnnotations(this.pdfKit)}return n}fakeElementId=0;}
|
||||
`)}},allowOutsideClick:()=>!Swal.isLoading()}).then(n=>{if(n.isConfirmed){const t=n.value;t.ok?reload():Swal.showValidationMessage(`Request failed: ${t.message}`)}});break;case"COPY_URL":const n=window.location.href.replace(/\/readonly/gi,"");navigator.clipboard.writeText(n).then(function(){bsNotify("Kopiert",{alert_type:"success",delay:4,icon_name:"check_circle"})}).catch(function(){bsNotify("Unerwarteter Fehler",{alert_type:"danger",delay:4,icon_name:"error"})});break;case"SHARE":Comp.ShareBackdrop.show();break;case"LOGOUT":await logout()}}async handleFinish(){let n=undefined;if(READ_AND_CONFIRM){const n=JSON.parse(sessionStorage.getItem("pspdf_all_pages_rendered")||"false")===!0;if(!n){const n=JSON.parse(sessionStorage.getItem("pspdf_unviewed_pages")||"[]"),t=n.length?`Bitte sehen Sie sich die folgenden Seiten an: ${n.join(", ")}`:"Bitte sehen Sie sich alle Seiten an.";return await Swal.fire({title:"Warnung",text:t,icon:"warning"}),!1}}else{const i=await this.pdfKit.exportInstantJSON(),r=i.formFieldValues,u=r.filter(n=>isFieldRequired(n)),f=u.some(n=>n.value===undefined||n.value===null||n.value==="");if(f)return Swal.fire({title:"Warnung",text:"Bitte füllen Sie alle Standortinformationen vollständig aus!",icon:"warning"}),!1;const e=new RegExp("^[a-zA-Z\\u0080-\\u024F]+(?:([\\ \\-\\']|(\\.\\ ))[a-zA-Z\\u0080-\\u024F]+)*$"),o=r.filter(n=>isCityField(n));for(var t of o)if(!IS_MOBILE_DEVICE&&!e.test(t.value))return Swal.fire({title:"Warnung",text:`Bitte überprüfen Sie die eingegebene Ortsangabe "${t.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"}),!1;const s=await this.validateAnnotations(this.signatureCount);if(s===!1)return Swal.fire({title:"Warnung",text:"Es wurden nicht alle Signaturfelder ausgefüllt!",icon:"warning"}),!1;n={instant:i,structured:mapSignature(i)}}return Swal.fire({title:localized.confirmation,html:`<div class="text-start fs-6 p-0 m-0">${localized.sigAgree}</div>`,icon:"question",showCancelButton:!0,confirmButtonColor:"#3085d6",cancelButtonColor:"#d33",confirmButtonText:localized.finalize,cancelButtonText:localized.back}).then(async t=>{if(t.isConfirmed){try{await this.pdfKit.save()}catch(i){return Swal.fire({title:"Fehler",text:"Umschlag konnte nicht signiert werden!",icon:"error"}),!1}try{const t=READ_AND_CONFIRM?await signEnvelope():await signEnvelope(n);if(t.ok)return!0;if(t.status===409)return Swal.fire({title:"Warnung",text:"Umschlag ist nicht mehr verfügbar.",icon:"warning"}),!1;if(t.status===423)Swal.fire({title:"Info",text:"Dokument wurde von einem Empfänger abgelehnt. Sie werden weitergeleitet...",icon:"info",timer:2e3,showConfirmButton:!1}).then(()=>{location.reload()});else throw new Error;}catch(i){return Swal.fire({title:"Fehler",text:"Umschlag konnte nicht signiert werden!",icon:"error"}),!1}}else return!1})}async validateAnnotations(n){const t=await getAnnotations(this.pdfKit),i=t.map(n=>n.toJS()).filter(n=>n.isSignature);return n<=i.length}async handleReset(){const n=Swal.fire({title:"Sind sie sicher?",text:"Wollen Sie das Dokument und alle erstellten Signaturen zurücksetzen?",icon:"question",showCancelButton:!0});if(n.isConfirmed){const n=await deleteAnnotations(this.pdfKit)}return n}fakeElementId=0;}
|
||||
@@ -1,106 +1,111 @@
|
||||
//#region parameters
|
||||
const env = Object.freeze({
|
||||
__lazyXsrfToken: new Lazy(() => document.getElementsByName('__RequestVerificationToken')[0].value),
|
||||
get xsrfToken() {
|
||||
return this.__lazyXsrfToken.value;
|
||||
}
|
||||
__lazyXsrfToken: new Lazy(() => document.getElementsByName('__RequestVerificationToken')[0].value),
|
||||
get xsrfToken() {
|
||||
return this.__lazyXsrfToken.value;
|
||||
}
|
||||
})
|
||||
|
||||
const url = Object.freeze({
|
||||
reject: `/api/annotation/reject`,
|
||||
share: `/api/readonly`
|
||||
reject: `/api/annotation/reject`,
|
||||
share: `/api/readonly`
|
||||
});
|
||||
//#endregion
|
||||
|
||||
//#region request helper methods
|
||||
function sendRequest(method, url, body = undefined) {
|
||||
const options = {
|
||||
credentials: 'include',
|
||||
method: method,
|
||||
headers: {
|
||||
'X-XSRF-TOKEN': env.xsrfToken
|
||||
const urlObj = new URL(url, window.location.origin);
|
||||
if (!urlObj.searchParams.has("envKey")) {
|
||||
urlObj.searchParams.set("envKey", ENV_KEY);
|
||||
}
|
||||
}
|
||||
|
||||
if (body !== undefined) {
|
||||
options.body = JSON.stringify(body);
|
||||
options.headers['Content-Type'] = 'application/json';
|
||||
}
|
||||
const options = {
|
||||
credentials: 'include',
|
||||
method: method,
|
||||
headers: {
|
||||
'X-XSRF-TOKEN': env.xsrfToken
|
||||
}
|
||||
}
|
||||
|
||||
return fetch(url, options);
|
||||
if (body !== undefined) {
|
||||
options.body = JSON.stringify(body);
|
||||
options.headers['Content-Type'] = 'application/json';
|
||||
}
|
||||
|
||||
return fetch(urlObj, options);
|
||||
}
|
||||
|
||||
function getRequest(url) {
|
||||
return sendRequest('GET', url);
|
||||
return sendRequest('GET', url);
|
||||
}
|
||||
|
||||
function getJson(url) {
|
||||
return sendRequest('GET', url).then(res => {
|
||||
if (res.ok)
|
||||
return res.json();
|
||||
throw new Error(`Request failed with status ${res.status}`);
|
||||
});
|
||||
return sendRequest('GET', url).then(res => {
|
||||
if (res.ok)
|
||||
return res.json();
|
||||
throw new Error(`Request failed with status ${res.status}`);
|
||||
});
|
||||
}
|
||||
|
||||
function postRequest(url, body = undefined) {
|
||||
return sendRequest('POST', url, body);
|
||||
return sendRequest('POST', url, body);
|
||||
}
|
||||
|
||||
function reload() {
|
||||
window.location.reload();
|
||||
window.location.reload();
|
||||
}
|
||||
|
||||
function redirect(url) {
|
||||
window.location.href = url;
|
||||
window.location.href = url;
|
||||
}
|
||||
//#endregion
|
||||
|
||||
//#region envelope
|
||||
function signEnvelope(annotations) {
|
||||
return postRequest(`/api/annotation`, annotations)
|
||||
return postRequest(`/api/annotation`, annotations)
|
||||
}
|
||||
|
||||
async function getAnnotationParams(leftInInch = 0, topInInch = 0, inchToPointFactor = 72) {
|
||||
const annotParams = await getJson("/api/Config/Annotations");
|
||||
const annotParams = await getJson("/api/Config/Annotations");
|
||||
|
||||
for (var key in annotParams) {
|
||||
var annot = annotParams[key];
|
||||
annot.width *= inchToPointFactor;
|
||||
annot.height *= inchToPointFactor;
|
||||
annot.left += leftInInch - 0.7;
|
||||
annot.left *= inchToPointFactor;
|
||||
annot.top += topInInch - 0.5;
|
||||
annot.top *= inchToPointFactor;
|
||||
}
|
||||
for (var key in annotParams) {
|
||||
var annot = annotParams[key];
|
||||
annot.width *= inchToPointFactor;
|
||||
annot.height *= inchToPointFactor;
|
||||
annot.left += leftInInch - 0.7;
|
||||
annot.left *= inchToPointFactor;
|
||||
annot.top += topInInch - 0.5;
|
||||
annot.top *= inchToPointFactor;
|
||||
}
|
||||
|
||||
return annotParams;
|
||||
return annotParams;
|
||||
}
|
||||
|
||||
function rejectEnvelope(reason) {
|
||||
return postRequest(url.reject, reason);
|
||||
return postRequest(url.reject, reason);
|
||||
}
|
||||
|
||||
function shareEnvelope(receiverMail, dateValid) {
|
||||
return postRequest(url.share, { receiverMail: receiverMail, dateValid: dateValid });
|
||||
return postRequest(url.share, { receiverMail: receiverMail, dateValid: dateValid });
|
||||
}
|
||||
//#endregion
|
||||
|
||||
async function setLanguage(language) {
|
||||
const hasLang = await getJson('/api/localization/lang')
|
||||
.then(langs => langs.includes(language));
|
||||
const hasLang = await getJson('/api/localization/lang')
|
||||
.then(langs => langs.includes(language));
|
||||
|
||||
if (hasLang)
|
||||
postRequest(`/api/localization/lang/${language}`)
|
||||
.then(response => {
|
||||
if (response.redirected)
|
||||
redirect(response.url);
|
||||
});
|
||||
if (hasLang)
|
||||
postRequest(`/api/localization/lang/${language}`)
|
||||
.then(response => {
|
||||
if (response.redirected)
|
||||
redirect(response.url);
|
||||
});
|
||||
}
|
||||
|
||||
function logout() {
|
||||
return postRequest(`/auth/logout`)
|
||||
.then(res => {
|
||||
if (res.ok)
|
||||
window.location.href = "/";
|
||||
});
|
||||
return postRequest(`/auth/logout`)
|
||||
.then(res => {
|
||||
if (res.ok)
|
||||
window.location.href = "/";
|
||||
});
|
||||
}
|
||||
@@ -1 +1 @@
|
||||
function sendRequest(n,t,i=undefined){const r={credentials:"include",method:n,headers:{"X-XSRF-TOKEN":env.xsrfToken}};return i!==undefined&&(r.body=JSON.stringify(i),r.headers["Content-Type"]="application/json"),fetch(t,r)}function getRequest(n){return sendRequest("GET",n)}function getJson(n){return sendRequest("GET",n).then(n=>{if(n.ok)return n.json();throw new Error(`Request failed with status ${n.status}`);})}function postRequest(n,t=undefined){return sendRequest("POST",n,t)}function reload(){window.location.reload()}function redirect(n){window.location.href=n}function signEnvelope(n){return postRequest(`/api/annotation`,n)}async function getAnnotationParams(n=0,t=0,i=72){var f,r;const u=await getJson("/api/Config/Annotations");for(f in u)r=u[f],r.width*=i,r.height*=i,r.left+=n-.7,r.left*=i,r.top+=t-.5,r.top*=i;return u}function rejectEnvelope(n){return postRequest(url.reject,n)}function shareEnvelope(n,t){return postRequest(url.share,{receiverMail:n,dateValid:t})}async function setLanguage(n){const t=await getJson("/api/localization/lang").then(t=>t.includes(n));t&&postRequest(`/api/localization/lang/${n}`).then(n=>{n.redirected&&redirect(n.url)})}function logout(){return postRequest(`/auth/logout`).then(n=>{n.ok&&(window.location.href="/")})}const env=Object.freeze({__lazyXsrfToken:new Lazy(()=>document.getElementsByName("__RequestVerificationToken")[0].value),get xsrfToken(){return this.__lazyXsrfToken.value}}),url=Object.freeze({reject:`/api/annotation/reject`,share:`/api/readonly`});
|
||||
function sendRequest(n,t,i=undefined){const r=new URL(t,window.location.origin);r.searchParams.has("envKey")||r.searchParams.set("envKey",ENV_KEY);const u={credentials:"include",method:n,headers:{"X-XSRF-TOKEN":env.xsrfToken}};return i!==undefined&&(u.body=JSON.stringify(i),u.headers["Content-Type"]="application/json"),fetch(r,u)}function getRequest(n){return sendRequest("GET",n)}function getJson(n){return sendRequest("GET",n).then(n=>{if(n.ok)return n.json();throw new Error(`Request failed with status ${n.status}`);})}function postRequest(n,t=undefined){return sendRequest("POST",n,t)}function reload(){window.location.reload()}function redirect(n){window.location.href=n}function signEnvelope(n){return postRequest(`/api/annotation`,n)}async function getAnnotationParams(n=0,t=0,i=72){var f,r;const u=await getJson("/api/Config/Annotations");for(f in u)r=u[f],r.width*=i,r.height*=i,r.left+=n-.7,r.left*=i,r.top+=t-.5,r.top*=i;return u}function rejectEnvelope(n){return postRequest(url.reject,n)}function shareEnvelope(n,t){return postRequest(url.share,{receiverMail:n,dateValid:t})}async function setLanguage(n){const t=await getJson("/api/localization/lang").then(t=>t.includes(n));t&&postRequest(`/api/localization/lang/${n}`).then(n=>{n.redirected&&redirect(n.url)})}function logout(){return postRequest(`/auth/logout`).then(n=>{n.ok&&(window.location.href="/")})}const env=Object.freeze({__lazyXsrfToken:new Lazy(()=>document.getElementsByName("__RequestVerificationToken")[0].value),get xsrfToken(){return this.__lazyXsrfToken.value}}),url=Object.freeze({reject:`/api/annotation/reject`,share:`/api/readonly`});
|
||||
@@ -16,7 +16,45 @@
|
||||
isEditableAnnotation: function (annotation) {
|
||||
return !(annotation.isSignature || annotation.description === 'FRAME')
|
||||
},
|
||||
});
|
||||
}).then((instance) => {
|
||||
|
||||
if (READ_AND_CONFIRM) {
|
||||
const totalPages = instance.totalPageCount || 0
|
||||
const storageKeyAll = 'pspdf_all_pages_rendered'
|
||||
const storageKeyUnviewed = 'pspdf_unviewed_pages'
|
||||
|
||||
let unviewed = totalPages > 0 ? Array.from({ length: totalPages }, (_, i) => i + 1) : []
|
||||
|
||||
const saveState = () => {
|
||||
sessionStorage.setItem(storageKeyUnviewed, JSON.stringify(unviewed))
|
||||
sessionStorage.setItem(storageKeyAll, JSON.stringify(unviewed.length === 0 && totalPages > 0))
|
||||
}
|
||||
|
||||
const markPageViewed = (pageIndex) => {
|
||||
const pageNumber = pageIndex + 1
|
||||
if (pageNumber < 1 || pageNumber > totalPages) return
|
||||
const idx = unviewed.indexOf(pageNumber)
|
||||
if (idx >= 0) {
|
||||
unviewed.splice(idx, 1)
|
||||
saveState()
|
||||
}
|
||||
}
|
||||
|
||||
// initial state in session storage
|
||||
saveState()
|
||||
|
||||
// mark the initially visible page
|
||||
const initialPage = instance.viewState?.currentPageIndex ?? 0
|
||||
markPageViewed(initialPage)
|
||||
|
||||
instance.addEventListener('viewState.currentPageIndex.change', (pageIndex) => {
|
||||
console.log('Active page:', pageIndex + 1)
|
||||
markPageViewed(pageIndex)
|
||||
})
|
||||
}
|
||||
|
||||
return instance
|
||||
})
|
||||
}
|
||||
|
||||
const allowedToolbarItems = [
|
||||
@@ -103,7 +141,7 @@ function getReadOnlyItems(callback) {
|
||||
}
|
||||
|
||||
function getMobileWritableItems(callback) {
|
||||
return [
|
||||
const items = [
|
||||
{
|
||||
type: 'custom',
|
||||
id: 'button-finish',
|
||||
@@ -127,8 +165,11 @@ function getMobileWritableItems(callback) {
|
||||
icon: `<svg width="25px" height="25px" viewBox="43.5 43.5 512 512" version="1.1" fill="currentColor" xml:space="preserve" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
|
||||
<path class="st0" d="M263.24,43.5c-117.36,0-212.5,95.14-212.5,212.5s95.14,212.5,212.5,212.5s212.5-95.14,212.5-212.5 S380.6,43.5,263.24,43.5z M367.83,298.36c17.18,17.18,17.18,45.04,0,62.23v0c-17.18,17.18-45.04,17.18-62.23,0l-42.36-42.36 l-42.36,42.36c-17.18,17.18-45.04,17.18-62.23,0v0c-17.18-17.18-17.18-45.04,0-62.23L201.01,256l-42.36-42.36 c-17.18-17.18-17.18-45.04,0-62.23v0c17.18-17.18,45.04-17.18,62.23,0l42.36,42.36l42.36-42.36c17.18-17.18,45.04-17.18,62.23,0v0 c17.18,17.18,17.18,45.04,0,62.23L325.46,256L367.83,298.36z" />
|
||||
</svg>`,
|
||||
},
|
||||
{
|
||||
}
|
||||
]
|
||||
|
||||
if (!READ_AND_CONFIRM) {
|
||||
items.push({
|
||||
type: 'custom',
|
||||
id: 'button-reset',
|
||||
className: 'button-reset',
|
||||
@@ -139,9 +180,11 @@ function getMobileWritableItems(callback) {
|
||||
icon: `<svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" fill="currentColor" class="bi bi-arrow-counterclockwise" viewBox="-1 -1 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>`,
|
||||
}
|
||||
];
|
||||
</svg>`
|
||||
})
|
||||
}
|
||||
|
||||
return items
|
||||
}
|
||||
|
||||
function getPresets() {
|
||||
|
||||
8
EnvelopeGenerator.Web/wwwroot/js/ui.min.js
vendored
8
EnvelopeGenerator.Web/wwwroot/js/ui.min.js
vendored
@@ -1,4 +1,4 @@
|
||||
function loadPSPDFKit(n,t,i,r){return PSPDFKit.load({inlineWorkers:!1,locale:r,licenseKey:i,styleSheets:["/css/site.css"],container:t,document:n,annotationPresets:getPresets(),electronicSignatures:{creationModes:["DRAW","TYPE","IMAGE"]},initialViewState:new PSPDFKit.ViewState({sidebarMode:PSPDFKit.SidebarMode.THUMBNAILS}),isEditableAnnotation:function(n){return!(n.isSignature||n.description==="FRAME")}})}function addToolbarItems(n,t){var i=n.toolbarItems.filter(n=>allowedToolbarItems.includes(n.type));i=IS_READONLY?i.concat(getReadOnlyItems(t)):i.concat(getWritableItems(t));IS_DESKTOP_SIZE||IS_READONLY||(i=i.concat(getMobileWritableItems(t)));n.setToolbarItems(i)}function getWritableItems(n){return[{type:"custom",id:"button-share",className:"button-share",title:"Teilen",onPress(){n("SHARE")},icon:`<svg width="30" height="30" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
function loadPSPDFKit(n,t,i,r){return PSPDFKit.load({inlineWorkers:!1,locale:r,licenseKey:i,styleSheets:["/css/site.css"],container:t,document:n,annotationPresets:getPresets(),electronicSignatures:{creationModes:["DRAW","TYPE","IMAGE"]},initialViewState:new PSPDFKit.ViewState({sidebarMode:PSPDFKit.SidebarMode.THUMBNAILS}),isEditableAnnotation:function(n){return!(n.isSignature||n.description==="FRAME")}}).then(n=>{if(READ_AND_CONFIRM){const t=n.totalPageCount||0,f="pspdf_all_pages_rendered",e="pspdf_unviewed_pages";let i=t>0?Array.from({length:t},(n,t)=>t+1):[];const r=()=>{sessionStorage.setItem(e,JSON.stringify(i)),sessionStorage.setItem(f,JSON.stringify(i.length===0&&t>0))},u=n=>{const u=n+1;if(!(u<1)&&!(u>t)){const f=i.indexOf(u);f>=0&&(i.splice(f,1),r())}};r();const o=n.viewState?.currentPageIndex??0;u(o);n.addEventListener("viewState.currentPageIndex.change",n=>{console.log("Active page:",n+1),u(n)})}return n})}function addToolbarItems(n,t){var i=n.toolbarItems.filter(n=>allowedToolbarItems.includes(n.type));i=IS_READONLY?i.concat(getReadOnlyItems(t)):i.concat(getWritableItems(t));IS_DESKTOP_SIZE||IS_READONLY||(i=i.concat(getMobileWritableItems(t)));n.setToolbarItems(i)}function getWritableItems(n){return[{type:"custom",id:"button-share",className:"button-share",title:"Teilen",onPress(){n("SHARE")},icon:`<svg width="30" height="30" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M20 13V17.5C20 20.5577 16 20.5 12 20.5C8 20.5 4 20.5577 4 17.5V13M12 3L12 15M12 3L16 7M12 3L8 7" stroke="#000000" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
</svg>`},{type:"custom",id:"button-logout",className:"button-logout",title:"logout",onPress(){n("LOGOUT")},icon:`<svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" fill="currentColor" class="bi bi-box-arrow-left" viewBox="0 0 16 16">
|
||||
<path fill-rule="evenodd" d="M6 12.5a.5.5 0 0 0 .5.5h8a.5.5 0 0 0 .5-.5v-9a.5.5 0 0 0-.5-.5h-8a.5.5 0 0 0-.5.5v2a.5.5 0 0 1-1 0v-2A1.5 1.5 0 0 1 6.5 2h8A1.5 1.5 0 0 1 16 3.5v9a1.5 1.5 0 0 1-1.5 1.5h-8A1.5 1.5 0 0 1 5 12.5v-2a.5.5 0 0 1 1 0z"/>
|
||||
@@ -6,12 +6,12 @@ function loadPSPDFKit(n,t,i,r){return PSPDFKit.load({inlineWorkers:!1,locale:r,l
|
||||
</svg>`},{type:"custom",id:"mock",className:"mock",title:"Mock",icon:`<svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" fill="currentColor" class="bi bi-box-arrow-left" viewBox="0 0 16 16"></svg>`}]}function getReadOnlyItems(n){return[{type:"custom",id:"button-copy-url",className:"button-copy-url",title:"Teilen",onPress(){n("COPY_URL")},icon:`<svg viewBox="4 0 22 22" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M15 3H9C6.79086 3 5 4.79086 5 7V15" stroke="#222222"/>
|
||||
<path d="M8.5 11.5C8.5 10.3156 8.50074 9.46912 8.57435 8.81625C8.64681 8.17346 8.78457 7.78051 9.01662 7.4781C9.14962 7.30477 9.30477 7.14962 9.4781 7.01662C9.78051 6.78457 10.1735 6.64681 10.8163 6.57435C11.4691 6.50074 12.3156 6.5 13.5 6.5C14.6844 6.5 15.5309 6.50074 16.1837 6.57435C16.8265 6.64681 17.2195 6.78457 17.5219 7.01662C17.6952 7.14962 17.8504 7.30477 17.9834 7.4781C18.2154 7.78051 18.3532 8.17346 18.4257 8.81625C18.4993 9.46912 18.5 10.3156 18.5 11.5V15.5C18.5 16.6844 18.4993 17.5309 18.4257 18.1837C18.3532 18.8265 18.2154 19.2195 17.9834 19.5219C17.8504 19.6952 17.6952 19.8504 17.5219 19.9834C17.2195 20.2154 16.8265 20.3532 16.1837 20.4257C15.5309 20.4993 14.6844 20.5 13.5 20.5C12.3156 20.5 11.4691 20.4993 10.8163 20.4257C10.1735 20.3532 9.78051 20.2154 9.4781 19.9834C9.30477 19.8504 9.14962 19.6952 9.01662 19.5219C8.78457 19.2195 8.64681 18.8265 8.57435 18.1837C8.50074 17.5309 8.5 16.6844 8.5 15.5V11.5Z" stroke="#222222"/>
|
||||
</svg>`}]}function getMobileWritableItems(n){return[{type:"custom",id:"button-finish",className:"button-finish",onPress(){n("FINISH")},icon:`<svg class="icon" aria-hidden="true" xmlns="http://www.w3.org/2000/svg" fill="currentColor" viewBox="-4 -4 26 26">
|
||||
</svg>`}]}function getMobileWritableItems(n){const t=[{type:"custom",id:"button-finish",className:"button-finish",onPress(){n("FINISH")},icon:`<svg class="icon" aria-hidden="true" xmlns="http://www.w3.org/2000/svg" fill="currentColor" viewBox="-4 -4 26 26">
|
||||
<path d="m10.036 8.278 9.258-7.79A1.979 1.979 0 0 0 18 0H2A1.987 1.987 0 0 0 .641.541l9.395 7.737Z" />
|
||||
<path d="M11.241 9.817c-.36.275-.801.425-1.255.427-.428 0-.845-.138-1.187-.395L0 2.6V14a2 2 0 0 0 2 2h16a2 2 0 0 0 2-2V2.5l-8.759 7.317Z" />
|
||||
</svg>`},{type:"custom",id:"button-reject",className:"button-reject",title:"Ablehnen",onPress(){n("REJECT")},icon:`<svg width="25px" height="25px" viewBox="43.5 43.5 512 512" version="1.1" fill="currentColor" xml:space="preserve" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
|
||||
<path class="st0" d="M263.24,43.5c-117.36,0-212.5,95.14-212.5,212.5s95.14,212.5,212.5,212.5s212.5-95.14,212.5-212.5 S380.6,43.5,263.24,43.5z M367.83,298.36c17.18,17.18,17.18,45.04,0,62.23v0c-17.18,17.18-45.04,17.18-62.23,0l-42.36-42.36 l-42.36,42.36c-17.18,17.18-45.04,17.18-62.23,0v0c-17.18-17.18-17.18-45.04,0-62.23L201.01,256l-42.36-42.36 c-17.18-17.18-17.18-45.04,0-62.23v0c17.18-17.18,45.04-17.18,62.23,0l42.36,42.36l42.36-42.36c17.18-17.18,45.04-17.18,62.23,0v0 c17.18,17.18,17.18,45.04,0,62.23L325.46,256L367.83,298.36z" />
|
||||
</svg>`},{type:"custom",id:"button-reset",className:"button-reset",title:"Zurücksetzen",onPress(){n("RESET")},icon:`<svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" fill="currentColor" class="bi bi-arrow-counterclockwise" viewBox="-1 -1 16 16">
|
||||
</svg>`}];return READ_AND_CONFIRM||t.push({type:"custom",id:"button-reset",className:"button-reset",title:"Zurücksetzen",onPress(){n("RESET")},icon:`<svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" fill="currentColor" class="bi bi-arrow-counterclockwise" viewBox="-1 -1 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>`}]}function getPresets(){const n=PSPDFKit.defaultAnnotationPresets;return n.ink={lineWidth:10},n.widget={readOnly:!0},n}const allowedToolbarItems=["sidebar-thumbnails","sidebar-document-ouline","sidebar-bookmarks","pager","pan","zoom-out","zoom-in","zoom-mode","spacer","search","export-pdf"];
|
||||
</svg>`}),t}function getPresets(){const n=PSPDFKit.defaultAnnotationPresets;return n.ink={lineWidth:10},n.widget={readOnly:!0},n}const allowedToolbarItems=["sidebar-thumbnails","sidebar-document-ouline","sidebar-bookmarks","pager","pan","zoom-out","zoom-in","zoom-mode","spacer","search","export-pdf"];
|
||||
@@ -4,14 +4,14 @@
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Datenschutzinformation für das Fernsignatursystem signFLOW</title>
|
||||
<title>Datenschutzinformation für das Fernsignatursystem: signFLOW</title>
|
||||
<link rel="stylesheet" href="css/privacy-policy.min.css">
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<header>
|
||||
<h1>Datenschutzinformation für das Fernsignatursystem signFLOW</h1>
|
||||
<p><strong>Stand:</strong> 19.09.2024</p>
|
||||
<p><strong>Stand:</strong> 18.11.2025</p>
|
||||
</header>
|
||||
|
||||
<section>
|
||||
@@ -55,7 +55,7 @@
|
||||
<h2>3. Datenerhebung</h2>
|
||||
<h3>3.1 Die folgenden Kategorien personenbezogener Daten werden verarbeitet</h3>
|
||||
<ul>
|
||||
<li>Namen: Vor- und Zunamen sowie Ihre digitale Unterschrift</li>
|
||||
<li>Namen: Benutzername, Vor- und Zunamen sowie Ihre digitale Unterschrift</li>
|
||||
<li>Kontaktdaten: Telefonnummer, Mobilfunknummer und E-Mail-Adresse</li>
|
||||
<li>Technische Daten: IP-Adresse, Zeitpunkt des Zugriffs oder Zugriffsversuchs</li>
|
||||
</ul>
|
||||
@@ -162,138 +162,6 @@
|
||||
<a href="https://www.bfdi.bund.de/DE/Service/Anschriften/Laender/Laender-node.html">Laender-node.html</a>
|
||||
</p>
|
||||
</section>
|
||||
|
||||
<section>
|
||||
<h2>6. Hinweisgebersystem</h2>
|
||||
<p>
|
||||
Die Einhaltung gesetzlicher Vorschriften und interner Richtlinien, einschließlich unseres Verhaltenskodexes
|
||||
sowie des Verhaltenskodexes für Geschäftspartner, hat für uns (die verarbeitende Stelle) oberste Priorität.
|
||||
Dies gilt sowohl für unseren eigenen Geschäftsbereich als auch für unsere Lieferketten.
|
||||
</p>
|
||||
<p>
|
||||
Es ist uns wichtig, Risiken frühzeitig zu identifizieren und Verstöße zu vermeiden. Wir möchten rechtzeitig
|
||||
geeignete Maßnahmen ergreifen, um mögliche Schäden für Betroffene, Kunden, Mitarbeiter, Geschäftspartner und
|
||||
unsere Unternehmensgruppe zu verhindern.
|
||||
</p>
|
||||
<p>
|
||||
Aus diesem Grund haben wir ein unabhängiges, neutrales und vertrauliches Hinweisgebersystem eingerichtet,
|
||||
das es internen und externen Hinweisgebenden ermöglicht, auch anonym Meldungen abzugeben. Durch unser
|
||||
transparentes Beschwerdeverfahren bieten wir insbesondere den Betroffenen, den Hinweisgebenden und den
|
||||
Mitarbeitenden, die an der Aufklärung der gemeldeten Vorfälle mitwirken, den größtmöglichen Schutz.
|
||||
</p>
|
||||
<p>
|
||||
Im Rahmen dieses Verfahrens können alle tatsächlichen und vermeintlichen Verstöße gegen gesetzliche
|
||||
Vorgaben, unseren Verhaltenskodex sowie den Verhaltenskodex für Geschäftspartner gemeldet werden. Auch
|
||||
menschenrechtliche oder umweltbezogene Risiken sowie Pflichtverletzungen entlang der gesamten Lieferkette
|
||||
unserer Konzernunternehmen und in unserem eigenen Geschäftsbereich können Gegenstand einer Meldung sein.
|
||||
</p>
|
||||
<p>
|
||||
Einheitliche und zügige Prozesse sowie eine vertrauliche und professionelle Bearbeitung der Hinweise durch
|
||||
interne Experten bilden die Grundlage dieses fairen Verfahrens. Benachteiligungen oder Bestrafungen von
|
||||
Hinweisgebenden sowie von Personen, die mit der Bearbeitung von Beschwerden und Hinweisen betraut sind,
|
||||
werden nicht toleriert.
|
||||
</p>
|
||||
|
||||
<h3>6.1 Zweck und Rechtsgrundlage der Datenverarbeitung</h3>
|
||||
<p>
|
||||
Der Zweck der Verarbeitung personenbezogener Daten besteht in der Verwaltung des Hinweisgebersystems, das
|
||||
auch die Aufdeckung schwerwiegender Verstöße oder potenzieller Verstöße gegen geltendes Recht sowie anderer
|
||||
ernsthafter Angelegenheiten umfasst. Die Verarbeitung dieser Daten ist notwendig, um rechtlichen
|
||||
Verpflichtungen nachzukommen, die uns auferlegt sind, gemäß Art. 6 Abs. 1 S. 1 lit. c) DSGVO. Dies
|
||||
bezieht
|
||||
sich auf das Gesetz, das den Schutz von Hinweisgebern verbessert (Hinweisgeberschutzgesetz - HinSchG).
|
||||
</p>
|
||||
<p>
|
||||
Zudem dient die Verarbeitung dem berechtigten Interesse, schwerwiegende Verstöße oder mögliche Verstöße
|
||||
gegen geltendes Recht sowie andere ernsthafte Angelegenheiten aufzudecken, gemäß Art. 6 Abs. 1 S. 1 lit.
|
||||
f)
|
||||
DSGVO.
|
||||
</p>
|
||||
<p>
|
||||
Im Hinblick auf die Verarbeitung besonderer Kategorien personenbezogener Daten ist diese auf Grundlage des
|
||||
Hinweisgeberschutzgesetzes aus Gründen eines erheblichen öffentlichen Interesses erforderlich, gemäß Art. 9
|
||||
Abs. 2 lit. g) DSGVO. Die Verarbeitung dieser besonderen Daten erfolgt gemäß Art. 9 Abs. 2 lit. f)
|
||||
DSGVO in
|
||||
Verbindung mit Art. 6 Abs. 1 S. 1 lit. f) DSGVO, um Rechtsansprüche festzustellen, auszuüben oder zu
|
||||
verteidigen.
|
||||
</p>
|
||||
<p>
|
||||
Betroffene Personen sind diejenigen, über die eine Meldung gemacht wird. Dies können Mitarbeiter,
|
||||
Vertragspartner oder andere Personen sein, die in beruflicher Verbindung zu der verarbeitenden Stelle
|
||||
stehen. Darüber hinaus verarbeiten wir personenbezogene Daten der hinweisgebenden Person, wenn diese ihre
|
||||
Kontaktinformationen oder andere identifizierende Informationen übermittelt. Hinweisgebende Personen sollten
|
||||
sich daher bewusst sein, dass wir im Rahmen der Bearbeitung des gemeldeten Falls personenbezogene Daten über
|
||||
sie verarbeiten können.
|
||||
</p>
|
||||
|
||||
<h3>6.2 Kategorien personenbezogener Daten</h3>
|
||||
<p>
|
||||
Die Meldung kann anonym erfolgen, wodurch keine personenbezogenen Daten der meldenden Person verarbeitet
|
||||
werden. Die Art der personenbezogenen Daten, die verarbeitet werden, hängt von den übermittelten
|
||||
Informationen ab. Wenn die meldende Person personenbezogene Daten über eine andere Person, einschließlich
|
||||
der gemeldeten Person oder Personen, angibt, werden auch diese Daten verarbeitet. Folgende Kategorien von
|
||||
personenbezogenen Daten können verarbeitet werden:
|
||||
</p>
|
||||
<ul>
|
||||
<li>Allgemeine personenbezogene Daten (z.B.: Vorname, Nachname, Adresse, E-Mail-Adresse, Telefonnummer,
|
||||
usw.)</li>
|
||||
<li>Personenbezogene Daten zu strafrechtlichen Verurteilungen oder Verdachtsmomenten</li>
|
||||
<li>Besondere Kategorien personenbezogener Daten (Informationen über rassische oder ethnische Herkunft,
|
||||
politische Meinungen, religiöse oder philosophische Überzeugungen, Gewerkschaftszugehörigkeit,
|
||||
Gesundheitsdaten sowie Informationen über das Sexualleben oder die sexuelle Orientierung einer Person)
|
||||
</li>
|
||||
</ul>
|
||||
<p>
|
||||
Wir bitten die meldende Person, ausschließlich Informationen zu übermitteln, die für den jeweiligen Fall von
|
||||
Bedeutung sind, und insbesondere keine sensiblen Informationen zu melden, es sei denn, diese sind für die
|
||||
Bearbeitung des gemeldeten Falls von zentraler Relevanz.
|
||||
</p>
|
||||
|
||||
<h3>6.3 Verpflichtung zur Bereitstellung personenbezogener Daten</h3>
|
||||
<p>
|
||||
Es ist nicht erforderlich, die im Abschnitt 6.2 genannten personenbezogenen Daten bereitzustellen, da auch
|
||||
eine anonyme Meldung möglich ist. Bitte beachte jedoch, dass wir möglicherweise nicht in der Lage sind, die
|
||||
Meldung zu bearbeiten, wenn keine personenbezogenen Daten angegeben werden.
|
||||
</p>
|
||||
|
||||
<h3>6.4 Empfänger personenbezogener Daten</h3>
|
||||
<p>Die Meldungen werden bei der verarbeitenden Stelle im System als Vorgänge erfasst. Nach einer Bewertung
|
||||
werden diese Vorgänge intern an die zuständigen Fachabteilungen weitergeleitet, und gegebenenfalls werden
|
||||
Folgemaßnahmen eingeleitet. Sollte eine Meldung eine der Konzerngesellschaften der verarbeitenden Stelle
|
||||
betreffen, werden die relevanten Vorgänge an die zuständigen Personen der jeweiligen Gesellschaft
|
||||
weitergegeben, die dann intern eine Bewertung vornehmen und gegebenenfalls Maßnahmen ergreifen. Bei der
|
||||
Weitergabe personenbezogener Daten wird der Grundsatz der Datenminimierung beachtet, was bedeutet, dass nur
|
||||
die unbedingt notwendigen Daten zur Bearbeitung der Meldung weitergegeben werden.</p>
|
||||
<p>Personenbezogene Daten der hinweisgebenden Person werden an Behörden weitergeleitet, wenn dies erforderlich
|
||||
ist, um schwerwiegende Verstöße oder Angelegenheiten zu behandeln oder das Recht auf Verteidigung der
|
||||
betroffenen Personen zu sichern. In anderen Fällen erfolgt die Weitergabe personenbezogener Daten der
|
||||
hinweisgebenden Person nur mit deren Zustimmung. Daten über andere Personen als die hinweisgebende Person
|
||||
werden nur im Rahmen der Nachverfolgung eines gemeldeten Falls oder zur Bearbeitung schwerwiegender Verstöße
|
||||
oder Angelegenheiten weitergegeben.</p>
|
||||
<p>Die Meldeplattform wird von dem Auftragsverarbeiter WhistleB Whistleblowing Centre AB mit Sitz in Stockholm,
|
||||
Schweden, bereitgestellt. Weitere Informationen zu WhistleB und den entsprechenden Nutzungsbedingungen sind
|
||||
dort einsehbar.
|
||||
<a
|
||||
href="https://report.whistleb.com/content/documents/whistleb_terms_of_use.pdf">whistleb_terms_of_use.pdf</a>
|
||||
</p>
|
||||
|
||||
<h3>6.5 Speicherdauer</h3>
|
||||
<p>Personenbezogene Daten, die sich als nicht relevant für die Bearbeitung eines gemeldeten Falls herausstellen,
|
||||
sowie Meldungen, die wir als unbegründet ansehen, werden umgehend als "nicht relevant" eingestuft. In diesem
|
||||
Fall wird der Personenbezug entfernt, es sei denn, es handelt sich bereits um eine anonyme Meldung. Um die
|
||||
gesetzlich vorgeschriebene Dokumentationspflicht und die Löschfristen gemäß § 11 Abs. 1 und Abs. 5 HinSchG
|
||||
zu erfüllen, wird die Meldung zunächst ohne Personenbezug archiviert, jedoch noch nicht gelöscht.
|
||||
Archivierte Fälle dienen ausschließlich der Erfüllung dieser Dokumentationspflichten und können danach nicht
|
||||
mehr zur Bearbeitung herangezogen werden.</p>
|
||||
<p>Die Meldungen und personenbezogenen Daten, die im Zuge der Bearbeitung einer Meldung erfasst werden, bilden
|
||||
die Grundlage für die weitere Bearbeitung und werden so schnell wie möglich anonymisiert. Sollte es
|
||||
notwendig sein, Folgemaßnahmen gemäß §§ 3 Abs. 8 und 18 HinSchG zu ergreifen, kann es jedoch erforderlich
|
||||
sein, von der Anonymisierung abzuweichen, sei es aufgrund behördlicher Anordnungen oder zur Wahrung von
|
||||
Rechtsansprüchen. In solchen Fällen wird in der Regel eine Pseudonymisierung angestrebt, es sei denn, es
|
||||
gibt andere Vorgaben, wie etwa eine richterliche Anordnung. Die Dokumentation wird drei Jahre nach Abschluss
|
||||
des Verfahrens gelöscht. Sie kann jedoch länger aufbewahrt werden, um den Anforderungen dieses Gesetzes oder
|
||||
anderer Rechtsvorschriften gerecht zu werden, solange dies notwendig und angemessen ist.</p>
|
||||
</section>
|
||||
</body>
|
||||
|
||||
</html>
|
||||
@@ -10,8 +10,8 @@
|
||||
|
||||
<body>
|
||||
<header>
|
||||
<h1>Data Protection Information for the Remote Signature System signFLOW</h1>
|
||||
<p><strong>As of:</strong> 19.09.2024</p>
|
||||
<h1>Data Protection Information for the Remote Signature System: signFLOW</h1>
|
||||
<p><strong>As of:</strong> 18.11.2025</p>
|
||||
</header>
|
||||
<section>
|
||||
<h2>1. General Information</h2>
|
||||
@@ -53,7 +53,7 @@
|
||||
<h2>3. Data Collection</h2>
|
||||
<h3>3.1 The following categories of personal data are processed</h3>
|
||||
<ul>
|
||||
<li>Names: First and last names as well as your digital signature</li>
|
||||
<li>Names: Username, first and last names as well as your digital signature</li>
|
||||
<li>Contact details: Phone number, mobile phone number, and email address</li>
|
||||
<li>Technical data: IP address, time of access, or access attempts</li>
|
||||
</ul>
|
||||
@@ -153,133 +153,6 @@
|
||||
<a href="https://www.bfdi.bund.de/DE/Service/Anschriften/Laender/Laender-node.html">Laender-node.html</a>
|
||||
</p>
|
||||
</section>
|
||||
|
||||
<section>
|
||||
<h2>6. Whistleblower System</h2>
|
||||
<p>
|
||||
Compliance with legal regulations and internal guidelines, including our Code of Conduct and the Code of
|
||||
Conduct for Business Partners, is our (the data processing entity's) top priority. This applies both to our
|
||||
own business operations and to our supply chains.
|
||||
</p>
|
||||
<p>
|
||||
It is important to us to identify risks early and avoid violations. We aim to take appropriate measures in a
|
||||
timely manner to prevent potential harm to affected persons, customers, employees, business partners, and
|
||||
our corporate group.
|
||||
</p>
|
||||
<p>
|
||||
For this reason, we have established an independent, neutral, and confidential whistleblower system that
|
||||
enables internal and external whistleblowers to submit reports, including anonymously. Through our
|
||||
transparent complaint procedure, we offer the greatest possible protection, especially to the affected
|
||||
persons, whistleblowers, and employees involved in investigating reported incidents.
|
||||
</p>
|
||||
<p>
|
||||
Under this procedure, any actual or alleged violations of legal requirements, our Code of Conduct, or the
|
||||
Code of Conduct for Business Partners may be reported. Human rights or environmental risks, as well as
|
||||
breaches of duty along the entire supply chain of our group companies and in our own business operations,
|
||||
can also be the subject of a report.
|
||||
</p>
|
||||
<p>
|
||||
Standardized and swift processes, as well as confidential and professional handling of the reports by
|
||||
internal experts, form the basis of this fair procedure. Discrimination or punishment of whistleblowers and
|
||||
individuals responsible for handling complaints and reports will not be tolerated.
|
||||
</p>
|
||||
|
||||
<h3>6.1 Purpose and Legal Basis of Data Processing</h3>
|
||||
<p>
|
||||
The purpose of processing personal data is to manage the whistleblower system, which also includes
|
||||
identifying serious violations or potential violations of applicable law and other serious matters. The
|
||||
processing of this data is necessary to comply with legal obligations imposed on us, in accordance with Art.
|
||||
6 para. 1 sentence 1 lit. c) GDPR. This refers to the law that enhances the protection of whistleblowers
|
||||
(Whistleblower Protection Act - HinSchG).
|
||||
</p>
|
||||
<p>
|
||||
Additionally, the processing serves the legitimate interest of identifying serious violations or potential
|
||||
violations of applicable law and other serious matters, in accordance with Art. 6 para. 1 sentence 1 lit. f)
|
||||
GDPR.
|
||||
</p>
|
||||
<p>
|
||||
Regarding the processing of special categories of personal data, this is necessary based on the
|
||||
Whistleblower Protection Act for reasons of significant public interest, in accordance with Art. 9 para. 2
|
||||
lit. g) GDPR. The processing of such special data is carried out in accordance with Art. 9 para. 2 lit. f)
|
||||
GDPR in conjunction with Art. 6 para. 1 sentence 1 lit. f) GDPR to establish, exercise, or defend legal
|
||||
claims.
|
||||
</p>
|
||||
<p>
|
||||
Affected persons are those about whom a report is made. These can be employees, contractors, or other
|
||||
individuals in a business relationship with the data processing entity. Furthermore, we process personal
|
||||
data of the whistleblower if they provide their contact details or other identifying information.
|
||||
Whistleblowers should be aware that we may process personal data about them during the handling of the
|
||||
reported case.
|
||||
</p>
|
||||
|
||||
<h3>6.2 Categories of Personal Data</h3>
|
||||
<p>
|
||||
Reports can be made anonymously, in which case no personal data of the reporting person will be processed.
|
||||
The type of personal data processed depends on the information provided. If the reporting person provides
|
||||
personal data about another individual, including the reported individual or persons, that data will also be
|
||||
processed. The following categories of personal data may be processed:
|
||||
</p>
|
||||
<ul>
|
||||
<li>General personal data (e.g., first name, last name, address, email address, phone number, etc.)</li>
|
||||
<li>Personal data related to criminal convictions or suspicions</li>
|
||||
<li>Special categories of personal data (information about racial or ethnic origin, political opinions,
|
||||
religious or philosophical beliefs, trade union membership, health data, and information about a
|
||||
person's sex life or sexual orientation)</li>
|
||||
</ul>
|
||||
<p>
|
||||
We ask the reporting person to only provide information relevant to the case and to avoid reporting
|
||||
sensitive information unless it is essential for handling the reported case.
|
||||
</p>
|
||||
|
||||
<h3>6.3 Obligation to Provide Personal Data</h3>
|
||||
<p>
|
||||
It is not mandatory to provide the personal data mentioned in section 6.2, as anonymous reporting is also
|
||||
possible. However, please note that we may be unable to process the report if no personal data is provided.
|
||||
</p>
|
||||
|
||||
<h3>6.4 Recipients of Personal Data</h3>
|
||||
<p>
|
||||
Reports are logged in the system of the data processing entity as cases. After evaluation, these cases are
|
||||
forwarded internally to the relevant departments, and follow-up actions may be initiated. If a report
|
||||
involves one of the group companies of the data processing entity, the relevant cases are forwarded to the
|
||||
responsible individuals at the respective company, who will then conduct an internal evaluation and take
|
||||
action if necessary. When transferring personal data, the principle of data minimization is observed,
|
||||
meaning only the data strictly necessary for handling the report is shared.
|
||||
</p>
|
||||
<p>
|
||||
Personal data of the whistleblower will be shared with authorities when necessary to address serious
|
||||
violations or issues, or to safeguard the right to defense of the affected persons. In other cases, personal
|
||||
data of the whistleblower will only be shared with their consent. Data about persons other than the
|
||||
whistleblower will only be shared in connection with the investigation of a reported case or to address
|
||||
serious violations or issues.
|
||||
</p>
|
||||
<p>
|
||||
The reporting platform is provided by the processor WhistleB Whistleblowing Centre AB, based in Stockholm,
|
||||
Sweden. Further information about WhistleB and the corresponding terms of use can be found at:
|
||||
<a
|
||||
href="https://report.whistleb.com/content/documents/whistleb_terms_of_use.pdf">whistleb_terms_of_use.pdf</a>
|
||||
</p>
|
||||
|
||||
<h3>6.5 Retention Period</h3>
|
||||
<p>
|
||||
Personal data that is found to be irrelevant to the processing of a reported case, as well as reports deemed
|
||||
unfounded, will be immediately classified as "not relevant." In this case, the personal reference is removed
|
||||
unless the report was anonymous from the outset. To meet the legally required documentation obligations and
|
||||
deletion periods pursuant to § 11 para. 1 and para. 5 HinSchG, the report is initially archived without
|
||||
personal reference but is not yet deleted. Archived cases serve solely to fulfill these documentation
|
||||
obligations and can no longer be used for further processing.
|
||||
</p>
|
||||
<p>
|
||||
Reports and personal data collected during the processing of a report form the basis for further handling
|
||||
and are anonymized as soon as possible. However, if it is necessary to take follow-up actions pursuant to §§
|
||||
3 para. 8 and 18 HinSchG, it may be necessary to deviate from anonymization, whether due to official orders
|
||||
or to protect legal claims. In such cases, pseudonymization is generally sought, unless other directives
|
||||
apply, such as a court order. Documentation is deleted three years after the conclusion of the process, but
|
||||
it may be retained longer if required to meet the requirements of this law or other legal provisions, as
|
||||
long as it remains necessary and appropriate.
|
||||
</p>
|
||||
</section>
|
||||
|
||||
</body>
|
||||
|
||||
</html>
|
||||
@@ -0,0 +1,13 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk.Worker">
|
||||
|
||||
<PropertyGroup>
|
||||
<TargetFramework>net8.0</TargetFramework>
|
||||
<Nullable>enable</Nullable>
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
<UserSecretsId>dotnet-EnvelopeGenerator.WorkerService-0636abb8-6085-477d-9f56-1a9787e84dde</UserSecretsId>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Microsoft.Extensions.Hosting" Version="8.0.1" />
|
||||
</ItemGroup>
|
||||
</Project>
|
||||
7
EnvelopeGenerator.WorkerService/Program.cs
Normal file
7
EnvelopeGenerator.WorkerService/Program.cs
Normal file
@@ -0,0 +1,7 @@
|
||||
using EnvelopeGenerator.WorkerService;
|
||||
|
||||
var builder = Host.CreateApplicationBuilder(args);
|
||||
builder.Services.AddHostedService<Worker>();
|
||||
|
||||
var host = builder.Build();
|
||||
host.Run();
|
||||
@@ -0,0 +1,12 @@
|
||||
{
|
||||
"$schema": "http://json.schemastore.org/launchsettings.json",
|
||||
"profiles": {
|
||||
"EnvelopeGenerator.WorkerService": {
|
||||
"commandName": "Project",
|
||||
"dotnetRunMessages": true,
|
||||
"environmentVariables": {
|
||||
"DOTNET_ENVIRONMENT": "Development"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
24
EnvelopeGenerator.WorkerService/Worker.cs
Normal file
24
EnvelopeGenerator.WorkerService/Worker.cs
Normal file
@@ -0,0 +1,24 @@
|
||||
namespace EnvelopeGenerator.WorkerService
|
||||
{
|
||||
public class Worker : BackgroundService
|
||||
{
|
||||
private readonly ILogger<Worker> _logger;
|
||||
|
||||
public Worker(ILogger<Worker> logger)
|
||||
{
|
||||
_logger = logger;
|
||||
}
|
||||
|
||||
protected override async Task ExecuteAsync(CancellationToken stoppingToken)
|
||||
{
|
||||
while (!stoppingToken.IsCancellationRequested)
|
||||
{
|
||||
if (_logger.IsEnabled(LogLevel.Information))
|
||||
{
|
||||
_logger.LogInformation("Worker running at: {time}", DateTimeOffset.Now);
|
||||
}
|
||||
await Task.Delay(1000, stoppingToken);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,8 @@
|
||||
{
|
||||
"Logging": {
|
||||
"LogLevel": {
|
||||
"Default": "Information",
|
||||
"Microsoft.Hosting.Lifetime": "Information"
|
||||
}
|
||||
}
|
||||
}
|
||||
8
EnvelopeGenerator.WorkerService/appsettings.json
Normal file
8
EnvelopeGenerator.WorkerService/appsettings.json
Normal file
@@ -0,0 +1,8 @@
|
||||
{
|
||||
"Logging": {
|
||||
"LogLevel": {
|
||||
"Default": "Information",
|
||||
"Microsoft.Hosting.Lifetime": "Information"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -25,8 +25,6 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "src", "src", "{134D4164-B29
|
||||
EndProject
|
||||
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "tests", "tests", "{0CBC2432-A561-4440-89BC-671B66A24146}"
|
||||
EndProject
|
||||
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "EnvelopeGenerator.Tests.Application", "EnvelopeGenerator.Tests.Application\EnvelopeGenerator.Tests.Application.csproj", "{A4D0DD1A-67BC-4E1A-AD29-BC4BC0D41399}"
|
||||
EndProject
|
||||
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "infrastructure", "infrastructure", "{02EA681E-C7D8-13C7-8484-4AC65E1B71E8}"
|
||||
EndProject
|
||||
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "presentation", "presentation", "{E3C758DC-914D-4B7E-8457-0813F1FDB0CB}"
|
||||
@@ -37,6 +35,12 @@ Project("{F184B08F-C81C-45F6-A57F-5ABD9991F28F}") = "EnvelopeGenerator.Form", "E
|
||||
EndProject
|
||||
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "EnvelopeGenerator.PdfEditor", "EnvelopeGenerator.PdfEditor\EnvelopeGenerator.PdfEditor.csproj", "{211619F5-AE25-4BA5-A552-BACAFE0632D3}"
|
||||
EndProject
|
||||
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "EnvelopeGenerator.Tests", "EnvelopeGenerator.Tests\EnvelopeGenerator.Tests.csproj", "{224C4845-1CDE-22B7-F3A9-1FF9297F70E8}"
|
||||
EndProject
|
||||
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "EnvelopeGenerator.Jobs", "EnvelopeGenerator.Jobs\EnvelopeGenerator.Jobs.csproj", "{3D0514EA-2681-4B13-AD71-35CC6363DBD7}"
|
||||
EndProject
|
||||
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "EnvelopeGenerator.WorkerService", "EnvelopeGenerator.WorkerService\EnvelopeGenerator.WorkerService.csproj", "{E3676510-7030-4E85-86E1-51E483E2A3B6}"
|
||||
EndProject
|
||||
Global
|
||||
GlobalSection(SolutionConfigurationPlatforms) = preSolution
|
||||
Debug|Any CPU = Debug|Any CPU
|
||||
@@ -75,10 +79,6 @@ Global
|
||||
{E5E12BA4-60C1-48BA-9053-0F8B62B38124}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||
{E5E12BA4-60C1-48BA-9053-0F8B62B38124}.Release|Any CPU.ActiveCfg = Debug|Any CPU
|
||||
{E5E12BA4-60C1-48BA-9053-0F8B62B38124}.Release|Any CPU.Build.0 = Debug|Any CPU
|
||||
{A4D0DD1A-67BC-4E1A-AD29-BC4BC0D41399}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||
{A4D0DD1A-67BC-4E1A-AD29-BC4BC0D41399}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||
{A4D0DD1A-67BC-4E1A-AD29-BC4BC0D41399}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||
{A4D0DD1A-67BC-4E1A-AD29-BC4BC0D41399}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||
{A9F9B431-BB9B-49B8-9E2C-0703634A653A}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||
{A9F9B431-BB9B-49B8-9E2C-0703634A653A}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||
{A9F9B431-BB9B-49B8-9E2C-0703634A653A}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||
@@ -91,6 +91,18 @@ Global
|
||||
{211619F5-AE25-4BA5-A552-BACAFE0632D3}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||
{211619F5-AE25-4BA5-A552-BACAFE0632D3}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||
{211619F5-AE25-4BA5-A552-BACAFE0632D3}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||
{224C4845-1CDE-22B7-F3A9-1FF9297F70E8}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||
{224C4845-1CDE-22B7-F3A9-1FF9297F70E8}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||
{224C4845-1CDE-22B7-F3A9-1FF9297F70E8}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||
{224C4845-1CDE-22B7-F3A9-1FF9297F70E8}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||
{3D0514EA-2681-4B13-AD71-35CC6363DBD7}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||
{3D0514EA-2681-4B13-AD71-35CC6363DBD7}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||
{3D0514EA-2681-4B13-AD71-35CC6363DBD7}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||
{3D0514EA-2681-4B13-AD71-35CC6363DBD7}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||
{E3676510-7030-4E85-86E1-51E483E2A3B6}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||
{E3676510-7030-4E85-86E1-51E483E2A3B6}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||
{E3676510-7030-4E85-86E1-51E483E2A3B6}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||
{E3676510-7030-4E85-86E1-51E483E2A3B6}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||
EndGlobalSection
|
||||
GlobalSection(SolutionProperties) = preSolution
|
||||
HideSolutionNode = FALSE
|
||||
@@ -105,12 +117,14 @@ Global
|
||||
{5A9984F8-51A2-4558-A415-EC5FEED7CF7D} = {9943209E-1744-4944-B1BA-4F87FC1A0EEB}
|
||||
{E5E12BA4-60C1-48BA-9053-0F8B62B38124} = {E3C758DC-914D-4B7E-8457-0813F1FDB0CB}
|
||||
{9943209E-1744-4944-B1BA-4F87FC1A0EEB} = {134D4164-B291-4E19-99B9-E4FA3AFAB62C}
|
||||
{A4D0DD1A-67BC-4E1A-AD29-BC4BC0D41399} = {0CBC2432-A561-4440-89BC-671B66A24146}
|
||||
{02EA681E-C7D8-13C7-8484-4AC65E1B71E8} = {134D4164-B291-4E19-99B9-E4FA3AFAB62C}
|
||||
{E3C758DC-914D-4B7E-8457-0813F1FDB0CB} = {134D4164-B291-4E19-99B9-E4FA3AFAB62C}
|
||||
{A9F9B431-BB9B-49B8-9E2C-0703634A653A} = {E3C758DC-914D-4B7E-8457-0813F1FDB0CB}
|
||||
{6D56C01F-D6CB-4D8A-BD3D-4FD34326998C} = {E3C758DC-914D-4B7E-8457-0813F1FDB0CB}
|
||||
{211619F5-AE25-4BA5-A552-BACAFE0632D3} = {9943209E-1744-4944-B1BA-4F87FC1A0EEB}
|
||||
{224C4845-1CDE-22B7-F3A9-1FF9297F70E8} = {0CBC2432-A561-4440-89BC-671B66A24146}
|
||||
{3D0514EA-2681-4B13-AD71-35CC6363DBD7} = {9943209E-1744-4944-B1BA-4F87FC1A0EEB}
|
||||
{E3676510-7030-4E85-86E1-51E483E2A3B6} = {9943209E-1744-4944-B1BA-4F87FC1A0EEB}
|
||||
EndGlobalSection
|
||||
GlobalSection(ExtensibilityGlobals) = postSolution
|
||||
SolutionGuid = {73E60370-756D-45AD-A19A-C40A02DACCC7}
|
||||
|
||||
Reference in New Issue
Block a user