Compare commits
79 Commits
ef28bbaaf1
...
feat/recei
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
e6b41f10c8 | ||
|
|
dc78ad4a24 | ||
| b282318298 | |||
| 8d68ea8c57 | |||
|
|
7cb8b02b1d | ||
|
|
cc1d33462c | ||
|
|
cd85b4fffc | ||
|
|
ab3e7fb4e9 | ||
|
|
4a7676765c | ||
|
|
562ceb9c3f | ||
|
|
751ea706df | ||
|
|
490ad9f7cf | ||
| 7aeaba7c12 | |||
| 2301a81a1c | |||
| fc23ba840e | |||
| 140d271b28 | |||
| a3b12a6957 | |||
| 16bdc7820d | |||
| 06e32b99ea | |||
| c7c78f96a6 | |||
| 5c232e61f2 | |||
| 24c9321c0f | |||
| c75c2b1dd5 | |||
| 8445757f34 | |||
| b088eb089f | |||
| e66c46767e | |||
| bc732d311c | |||
| c90d29d654 | |||
| 47a2e950ca | |||
| 6ef989213e | |||
| 2a27b6161b | |||
| efdc372b04 | |||
| 698b7ca1ac | |||
| bf6947a28c | |||
| e2e31e2e69 | |||
| 73f6221c3c | |||
| 10f730a833 | |||
| cf5a301942 | |||
| e364f1f592 | |||
| 8a488d4e71 | |||
| f0be1a5b03 | |||
| 773721b634 | |||
| 99e3e4c24d | |||
| b9c86ce3c6 | |||
| 637b45efe0 | |||
| 28b8c311f9 | |||
| 00c7fe5316 | |||
| e5a061d5b5 | |||
| 629b02863b | |||
| 3b24755c35 | |||
| 864e9e8164 | |||
| 7eff958d0a | |||
| c3deaae63b | |||
| bb0197e6ba | |||
| ec2935b524 | |||
| 4fd7982cba | |||
| ddcf5edc00 | |||
| 74d207caa3 | |||
| a367c12551 | |||
| 35a328f8dc | |||
| d259a15b4b | |||
| 23e0e5ddbe | |||
| 0bb85c28c1 | |||
| a11d9a0e0e | |||
| b9bb058137 | |||
| 0818d7d9eb | |||
| 9d200800c5 | |||
| 6feb601670 | |||
| 39c12ada45 | |||
| 985ad4dc29 | |||
| 038ac2aed0 | |||
| 5e74de0ce7 | |||
| 0ce7ae9494 | |||
| 7041a4694a | |||
| 75e47d10e3 | |||
| 7f9125b3aa | |||
| fee256a51a | |||
| 8ad7c37261 | |||
| 1f745ae79c |
1
.vscode/settings.json
vendored
Normal file
1
.vscode/settings.json
vendored
Normal file
@@ -0,0 +1 @@
|
||||
{}
|
||||
@@ -24,6 +24,26 @@ public record AnnotationCreateDto
|
||||
///
|
||||
/// </summary>
|
||||
public string Type { get; init; } = null!;
|
||||
|
||||
/// <summary>
|
||||
///
|
||||
/// </summary>
|
||||
public double? X { get; init; }
|
||||
|
||||
/// <summary>
|
||||
///
|
||||
/// </summary>
|
||||
public double? Y { get; init; }
|
||||
|
||||
/// <summary>
|
||||
///
|
||||
/// </summary>
|
||||
public double? Width { get; init; }
|
||||
|
||||
/// <summary>
|
||||
///
|
||||
/// </summary>
|
||||
public double? Height { get; init; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
||||
@@ -82,7 +82,7 @@ public record EnvelopeDto
|
||||
/// <summary>
|
||||
///
|
||||
/// </summary>
|
||||
public bool? UseAccessCode { get; set; }
|
||||
public bool UseAccessCode { get; set; } = true;
|
||||
|
||||
/// <summary>
|
||||
///
|
||||
|
||||
@@ -0,0 +1,101 @@
|
||||
using DigitalData.Core.Abstraction.Application.Repository;
|
||||
using DigitalData.Core.Exceptions;
|
||||
using EnvelopeGenerator.Domain.Constants;
|
||||
using EnvelopeGenerator.Domain.Entities;
|
||||
using MediatR;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
|
||||
namespace EnvelopeGenerator.Application.Histories.Queries;
|
||||
|
||||
//TODO: Add sender query
|
||||
/// <summary>
|
||||
/// Repräsentiert eine Abfrage für die Verlaufshistorie eines Umschlags.
|
||||
/// </summary>
|
||||
public record CountHistoryQuery : HistoryQueryBase, IRequest<int>;
|
||||
|
||||
/// <summary>
|
||||
///
|
||||
/// </summary>
|
||||
public static class CountHistoryQueryExtensions
|
||||
{
|
||||
/// <summary>
|
||||
///
|
||||
/// </summary>
|
||||
/// <param name="sender"></param>
|
||||
/// <param name="uuid"></param>
|
||||
/// <param name="statuses"></param>
|
||||
/// <param name="cancel"></param>
|
||||
/// <returns></returns>
|
||||
public static async Task<bool> AnyHistoryAsync(this ISender sender, string uuid, IEnumerable<EnvelopeStatus> statuses, CancellationToken cancel = default)
|
||||
{
|
||||
var count = await sender.Send(new CountHistoryQuery
|
||||
{
|
||||
Envelope = new() { Uuid = uuid },
|
||||
Statuses = new() { Include = statuses }
|
||||
}, cancel);
|
||||
return count > 0;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
///
|
||||
/// </summary>
|
||||
public class CountHistoryQueryHandler : IRequestHandler<CountHistoryQuery, int>
|
||||
{
|
||||
private readonly IRepository<History> _repo;
|
||||
|
||||
/// <summary>
|
||||
///
|
||||
/// </summary>
|
||||
/// <param name="repo"></param>
|
||||
public CountHistoryQueryHandler(IRepository<History> repo)
|
||||
{
|
||||
_repo = repo;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
///
|
||||
/// </summary>
|
||||
/// <param name="request"></param>
|
||||
/// <param name="cancel"></param>
|
||||
/// <returns></returns>
|
||||
/// <exception cref="NotFoundException"></exception>
|
||||
public Task<int> Handle(CountHistoryQuery request, CancellationToken cancel = default)
|
||||
{
|
||||
var query = _repo.Query;
|
||||
|
||||
if (request.Envelope.Id is int envId)
|
||||
query = query.Where(e => e.Id == envId);
|
||||
else if (request.Envelope.Uuid is string uuid)
|
||||
query = query.Where(e => e.Envelope!.Uuid == uuid);
|
||||
#pragma warning disable CS0618 // Type or member is obsolete
|
||||
else if (request.EnvelopeId is not null)
|
||||
query = query.Where(h => h.EnvelopeId == request.EnvelopeId);
|
||||
#pragma warning restore CS0618 // Type or member is obsolete
|
||||
else
|
||||
throw new BadRequestException("Invalid request: An Envelope object or a valid EnvelopeId/UUID must be supplied.");
|
||||
|
||||
#pragma warning disable CS0618 // Type or member is obsolete
|
||||
if (request.Status is not null)
|
||||
query = query.Where(h => h.Status == request.Status);
|
||||
#pragma warning restore CS0618 // Type or member is obsolete
|
||||
|
||||
if (request.Statuses is not null)
|
||||
{
|
||||
var status = request.Statuses;
|
||||
if (status.Min is not null)
|
||||
query = query.Where(er => er.Envelope!.Status >= status.Min);
|
||||
|
||||
if (status.Max is not null)
|
||||
query = query.Where(er => er.Envelope!.Status <= status.Max);
|
||||
|
||||
if (status.Include?.Count() > 0)
|
||||
query = query.Where(er => status.Include.Contains(er.Envelope!.Status));
|
||||
|
||||
if (status.Ignore is not null)
|
||||
query = query.Where(er => !status.Ignore.Contains(er.Envelope!.Status));
|
||||
}
|
||||
|
||||
return query.CountAsync(cancel);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,60 @@
|
||||
using EnvelopeGenerator.Application.Common.Query;
|
||||
using EnvelopeGenerator.Domain.Constants;
|
||||
using System.ComponentModel.DataAnnotations;
|
||||
|
||||
namespace EnvelopeGenerator.Application.Histories.Queries;
|
||||
|
||||
//TODO: Add sender query
|
||||
/// <summary>
|
||||
///
|
||||
/// </summary>
|
||||
public record HistoryQueryBase
|
||||
{
|
||||
/// <summary>
|
||||
/// Die eindeutige Kennung des Umschlags.
|
||||
/// </summary>
|
||||
[Obsolete("Use Envelope property")]
|
||||
public int? EnvelopeId { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Der Include des Umschlags, der abgefragt werden soll. Kann optional angegeben werden, um die Ergebnisse zu filtern.
|
||||
/// </summary>
|
||||
[Obsolete("Use statuses")]
|
||||
public EnvelopeStatus? Status { get; set; }
|
||||
|
||||
/// <summary>
|
||||
///
|
||||
/// </summary>
|
||||
public EnvelopeStatusQuery Statuses { get; set; } = new();
|
||||
|
||||
/// <summary>
|
||||
///
|
||||
/// </summary>
|
||||
public EnvelopeQueryBase Envelope { get; set; } = new EnvelopeQueryBase();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
///
|
||||
/// </summary>
|
||||
public record EnvelopeStatusQuery
|
||||
{
|
||||
/// <summary>
|
||||
/// Der minimale Statuswert, der berücksichtigt werden.
|
||||
/// </summary>
|
||||
public EnvelopeStatus? Min { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Der maximale Statuswert, der berücksichtigt werden.
|
||||
/// </summary>
|
||||
public EnvelopeStatus? Max { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Eine Liste von Statuswerten, die einbezogen werden.
|
||||
/// </summary>
|
||||
public IEnumerable<EnvelopeStatus>? Include { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Eine Liste von Statuswerten, die ignoriert werden werden.
|
||||
/// </summary>
|
||||
public IEnumerable<EnvelopeStatus>? Ignore { get; init; }
|
||||
}
|
||||
@@ -1,7 +1,10 @@
|
||||
using EnvelopeGenerator.Application.Common.Dto.History;
|
||||
using EnvelopeGenerator.Domain.Constants;
|
||||
using AutoMapper;
|
||||
using DigitalData.Core.Abstraction.Application.Repository;
|
||||
using DigitalData.Core.Exceptions;
|
||||
using EnvelopeGenerator.Application.Common.Dto.History;
|
||||
using MediatR;
|
||||
using System.ComponentModel.DataAnnotations;
|
||||
using EnvelopeGenerator.Domain.Entities;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
|
||||
namespace EnvelopeGenerator.Application.Histories.Queries;
|
||||
|
||||
@@ -9,21 +12,81 @@ namespace EnvelopeGenerator.Application.Histories.Queries;
|
||||
/// <summary>
|
||||
/// Repräsentiert eine Abfrage für die Verlaufshistorie eines Umschlags.
|
||||
/// </summary>
|
||||
public record ReadHistoryQuery : IRequest<IEnumerable<HistoryDto>>
|
||||
public record ReadHistoryQuery : HistoryQueryBase, IRequest<IEnumerable<HistoryDto>>
|
||||
{
|
||||
/// <summary>
|
||||
/// Die eindeutige Kennung des Umschlags.
|
||||
/// </summary>
|
||||
[Required]
|
||||
public int EnvelopeId { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Der Include des Umschlags, der abgefragt werden soll. Kann optional angegeben werden, um die Ergebnisse zu filtern.
|
||||
/// </summary>
|
||||
public EnvelopeStatus? Status { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Abfrage zur Steuerung, ob nur der aktuelle Include oder der gesamte Datensatz zurückgegeben wird.
|
||||
/// </summary>
|
||||
public bool? OnlyLast { get; init; } = true;
|
||||
public bool OnlyLast { get; init; } = true;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
///
|
||||
/// </summary>
|
||||
public class ReadHistoryQueryHandler : IRequestHandler<ReadHistoryQuery, IEnumerable<HistoryDto>>
|
||||
{
|
||||
private readonly IRepository<History> _repo;
|
||||
|
||||
private readonly IMapper _mapper;
|
||||
|
||||
/// <summary>
|
||||
///
|
||||
/// </summary>
|
||||
/// <param name="repo"></param>
|
||||
/// <param name="mapper"></param>
|
||||
public ReadHistoryQueryHandler(IRepository<History> repo, IMapper mapper)
|
||||
{
|
||||
_repo = repo;
|
||||
_mapper = mapper;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
///
|
||||
/// </summary>
|
||||
/// <param name="request"></param>
|
||||
/// <param name="cancel"></param>
|
||||
/// <returns></returns>
|
||||
/// <exception cref="NotFoundException"></exception>
|
||||
public async Task<IEnumerable<HistoryDto>> Handle(ReadHistoryQuery request, CancellationToken cancel = default)
|
||||
{
|
||||
var query = _repo.Query;
|
||||
|
||||
if (request.Envelope.Id is int envId)
|
||||
query = query.Where(e => e.Id == envId);
|
||||
else if (request.Envelope.Uuid is string uuid)
|
||||
query = query.Where(e => e.Envelope!.Uuid == uuid);
|
||||
#pragma warning disable CS0618 // Type or member is obsolete
|
||||
else if (request.EnvelopeId is not null)
|
||||
query = query.Where(h => h.EnvelopeId == request.EnvelopeId);
|
||||
#pragma warning restore CS0618 // Type or member is obsolete
|
||||
else
|
||||
throw new BadRequestException("Invalid request: An Envelope object or a valid EnvelopeId/UUID must be supplied.");
|
||||
|
||||
#pragma warning disable CS0618 // Type or member is obsolete
|
||||
if (request.Status is not null)
|
||||
query = query.Where(h => h.Status == request.Status);
|
||||
#pragma warning restore CS0618 // Type or member is obsolete
|
||||
|
||||
if (request.Statuses is not null)
|
||||
{
|
||||
var status = request.Statuses;
|
||||
if (status.Min is not null)
|
||||
query = query.Where(er => er.Envelope!.Status >= status.Min);
|
||||
|
||||
if (status.Max is not null)
|
||||
query = query.Where(er => er.Envelope!.Status <= status.Max);
|
||||
|
||||
if (status.Include?.Count() > 0)
|
||||
query = query.Where(er => status.Include.Contains(er.Envelope!.Status));
|
||||
|
||||
if (status.Ignore is not null)
|
||||
query = query.Where(er => !status.Ignore.Contains(er.Envelope!.Status));
|
||||
}
|
||||
|
||||
if (request.OnlyLast)
|
||||
query = query.OrderByDescending(x => x.AddedWhen);
|
||||
|
||||
var hists = await query.ToListAsync(cancel);
|
||||
return _mapper.Map<List<HistoryDto>>(hists);
|
||||
}
|
||||
}
|
||||
@@ -1,47 +0,0 @@
|
||||
using AutoMapper;
|
||||
using DigitalData.Core.Abstraction.Application.Repository;
|
||||
using DigitalData.Core.Exceptions;
|
||||
using EnvelopeGenerator.Application.Common.Dto.History;
|
||||
using EnvelopeGenerator.Domain.Entities;
|
||||
using MediatR;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
|
||||
namespace EnvelopeGenerator.Application.Histories.Queries;
|
||||
|
||||
/// <summary>
|
||||
///
|
||||
/// </summary>
|
||||
public class ReadHistoryQueryHandler : IRequestHandler<ReadHistoryQuery, IEnumerable<HistoryDto>>
|
||||
{
|
||||
private readonly IRepository<History> _repo;
|
||||
|
||||
private readonly IMapper _mapper;
|
||||
|
||||
/// <summary>
|
||||
///
|
||||
/// </summary>
|
||||
/// <param name="repo"></param>
|
||||
/// <param name="mapper"></param>
|
||||
public ReadHistoryQueryHandler(IRepository<History> repo, IMapper mapper)
|
||||
{
|
||||
_repo = repo;
|
||||
_mapper = mapper;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
///
|
||||
/// </summary>
|
||||
/// <param name="request"></param>
|
||||
/// <param name="cancel"></param>
|
||||
/// <returns></returns>
|
||||
/// <exception cref="NotFoundException"></exception>
|
||||
public async Task<IEnumerable<HistoryDto>> Handle(ReadHistoryQuery request, CancellationToken cancel = default)
|
||||
{
|
||||
var query = _repo.ReadOnly().Where(h => h.EnvelopeId == request.EnvelopeId);
|
||||
if (request.Status is not null)
|
||||
query = query.Where(h => h.Status == request.Status);
|
||||
|
||||
var hists = await query.ToListAsync(cancel);
|
||||
return _mapper.Map<List<HistoryDto>>(hists);
|
||||
}
|
||||
}
|
||||
@@ -73,8 +73,8 @@
|
||||
<Reference Include="DigitalData.Core.Abstraction.Application, Version=1.4.0.0, Culture=neutral, processorArchitecture=MSIL">
|
||||
<HintPath>..\packages\DigitalData.Core.Abstraction.Application.1.4.0\lib\net462\DigitalData.Core.Abstraction.Application.dll</HintPath>
|
||||
</Reference>
|
||||
<Reference Include="DigitalData.Core.Abstractions, Version=4.2.0.0, Culture=neutral, processorArchitecture=MSIL">
|
||||
<HintPath>..\packages\DigitalData.Core.Abstractions.4.2.0\lib\net462\DigitalData.Core.Abstractions.dll</HintPath>
|
||||
<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>
|
||||
</Reference>
|
||||
<Reference Include="DigitalData.Modules.Base, Version=1.3.8.0, Culture=neutral, processorArchitecture=MSIL">
|
||||
<HintPath>..\packages\DigitalData.Modules.Base.1.3.8\lib\net462\DigitalData.Modules.Base.dll</HintPath>
|
||||
|
||||
@@ -30,7 +30,9 @@ Public Class frmFinalizePDF
|
||||
Database = New MSSQLServer(LogConfig, dCnnStr)
|
||||
|
||||
#Disable Warning BC40000 ' Type or member is obsolete
|
||||
Factory.Shared.AddEnvelopeGeneratorInfrastructureServices(
|
||||
Factory.Shared _
|
||||
.BehaveOnPostBuild(PostBuildBehavior.Ignore) _
|
||||
.AddEnvelopeGeneratorInfrastructureServices(
|
||||
Sub(opt)
|
||||
opt.AddDbTriggerParams(
|
||||
Sub(triggers)
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
<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.Abstractions" version="4.2.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" />
|
||||
<package id="DigitalData.Modules.Database" version="2.3.5.4" targetFramework="net462" />
|
||||
|
||||
@@ -75,8 +75,9 @@
|
||||
<Reference Include="DigitalData.Core.Abstraction.Application, Version=1.4.0.0, Culture=neutral, processorArchitecture=MSIL">
|
||||
<HintPath>..\packages\DigitalData.Core.Abstraction.Application.1.4.0\lib\net462\DigitalData.Core.Abstraction.Application.dll</HintPath>
|
||||
</Reference>
|
||||
<Reference Include="DigitalData.Core.Abstractions, Version=4.2.0.0, Culture=neutral, processorArchitecture=MSIL">
|
||||
<HintPath>..\packages\DigitalData.Core.Abstractions.4.2.0\lib\net462\DigitalData.Core.Abstractions.dll</HintPath>
|
||||
<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>
|
||||
<Private>True</Private>
|
||||
</Reference>
|
||||
<Reference Include="DigitalData.Modules.Base, Version=1.3.8.0, Culture=neutral, processorArchitecture=MSIL">
|
||||
<HintPath>..\packages\DigitalData.Modules.Base.1.3.8\lib\net462\DigitalData.Modules.Base.dll</HintPath>
|
||||
|
||||
@@ -70,7 +70,9 @@ Namespace Jobs
|
||||
Database = New MSSQLServer(LogConfig, MSSQLServer.DecryptConnectionString(oConnectionString))
|
||||
|
||||
#Disable Warning BC40000 ' Type or member is obsolete
|
||||
Factory.Shared.AddEnvelopeGeneratorInfrastructureServices(
|
||||
Factory.Shared _
|
||||
.BehaveOnPostBuild(PostBuildBehavior.Ignore) _
|
||||
.AddEnvelopeGeneratorInfrastructureServices(
|
||||
Sub(opt)
|
||||
opt.AddDbTriggerParams(
|
||||
Sub(triggers)
|
||||
|
||||
@@ -36,21 +36,87 @@ Namespace Jobs.FinalizeDocument
|
||||
#Region "Burn PDF"
|
||||
Public Function BurnAnnotsToPDF(pSourceBuffer As Byte(), pInstantJSONList As List(Of String), envelopeId As Integer) As Byte()
|
||||
'read the elements of envelope with their annotations
|
||||
Dim sigRepo = Factory.Shared.Repository(Of Signature)()
|
||||
Dim elements = sigRepo _
|
||||
.Where(Function(sig) sig.Document.EnvelopeId = envelopeId) _
|
||||
.Include(Function(sig) sig.Annotations) _
|
||||
.ToList()
|
||||
Using scope = Factory.Shared.ScopeFactory.CreateScope()
|
||||
Dim sigRepo = scope.ServiceProvider.Repository(Of Signature)()
|
||||
Dim elements = sigRepo _
|
||||
.Where(Function(sig) sig.Document.EnvelopeId = envelopeId) _
|
||||
.Include(Function(sig) sig.Annotations) _
|
||||
.ToList()
|
||||
|
||||
Return If(elements.Any(),
|
||||
Return If(elements.Any(),
|
||||
BurnElementAnnotsToPDF(pSourceBuffer, elements),
|
||||
BurnInstantJSONAnnotsToPDF(pSourceBuffer, pInstantJSONList))
|
||||
End Using
|
||||
End Function
|
||||
|
||||
Public Function BurnElementAnnotsToPDF(pSourceBuffer As Byte(), elements As List(Of Signature)) As Byte()
|
||||
' Add background
|
||||
Using doc As Pdf(Of MemoryStream, MemoryStream) = Pdf.FromMemory(pSourceBuffer)
|
||||
Return doc.Background(elements).ExportStream().ToArray()
|
||||
'TODO: take the length from the largest y
|
||||
pSourceBuffer = doc.Background(elements, 1.9500000000000002 * 0.93, 2.52 * 0.67).ExportStream().ToArray()
|
||||
|
||||
Dim oResult As GdPictureStatus
|
||||
Using oSourceStream As New MemoryStream(pSourceBuffer)
|
||||
' Open PDF
|
||||
oResult = Manager.InitFromStream(oSourceStream)
|
||||
If oResult <> GdPictureStatus.OK Then
|
||||
Throw New BurnAnnotationException($"Could not open document for burning: [{oResult}]")
|
||||
End If
|
||||
|
||||
' Imported from background (add to configuration)
|
||||
Dim margin As Double = 0.2
|
||||
Dim inchFactor As Double = 72
|
||||
|
||||
' Y offset of form fields
|
||||
Dim keys = {"position", "city", "date"} ' add to configuration
|
||||
Dim unitYOffsets = 0.2
|
||||
Dim yOffsetsOfFF = keys.
|
||||
Select(Function(k, i) New With {Key .Key = k, Key .Value = unitYOffsets * i + 1}).
|
||||
ToDictionary(Function(x) x.Key, Function(x) x.Value)
|
||||
|
||||
'Add annotations
|
||||
For Each element In elements
|
||||
|
||||
Dim frameX = (element.Left - 0.7 - margin)
|
||||
|
||||
Dim frame = element.Annotations.FirstOrDefault(Function(a) a.Name = "frame")
|
||||
Dim frameY = element.Top - 0.5 - margin
|
||||
Dim frameYShift = frame.Y - frameY * inchFactor
|
||||
Dim frameXShift = frame.X - frameX * inchFactor
|
||||
|
||||
For Each annot In element.Annotations
|
||||
Dim yOffsetofFF As Double = If(yOffsetsOfFF.TryGetValue(annot.Name, yOffsetofFF), yOffsetofFF, 0)
|
||||
Dim y = frameY + yOffsetofFF
|
||||
|
||||
If annot.Type = AnnotationType.FormField Then
|
||||
AddFormFieldValue(annot.X / inchFactor, y, annot.Width / inchFactor, annot.Height / inchFactor, element.Page, annot.Value)
|
||||
ElseIf annot.Type = AnnotationType.Image Then
|
||||
AddImageAnnotation(
|
||||
annot.X / inchFactor,
|
||||
If(annot.Name = "signature", (annot.Y - frameYShift) / inchFactor, y),
|
||||
annot.Width / inchFactor,
|
||||
annot.Height / inchFactor,
|
||||
element.Page,
|
||||
annot.Value
|
||||
)
|
||||
ElseIf annot.Type = AnnotationType.Ink Then
|
||||
AddInkAnnotation(element.Page, annot.Value)
|
||||
End If
|
||||
Next
|
||||
Next
|
||||
|
||||
'Save PDF
|
||||
Using oNewStream As New MemoryStream()
|
||||
oResult = Manager.SaveDocumentToPDF(oNewStream)
|
||||
If oResult <> GdPictureStatus.OK Then
|
||||
Throw New BurnAnnotationException($"Could not save document to stream: [{oResult}]")
|
||||
End If
|
||||
|
||||
Manager.Close()
|
||||
|
||||
Return oNewStream.ToArray()
|
||||
End Using
|
||||
End Using
|
||||
End Using
|
||||
End Function
|
||||
|
||||
@@ -121,6 +187,11 @@ Namespace Jobs.FinalizeDocument
|
||||
|
||||
End Sub
|
||||
|
||||
Private Sub AddImageAnnotation(x As Double, y As Double, width As Double, height As Double, page As Integer, base64 As String)
|
||||
Manager.SelectPage(page)
|
||||
Manager.AddEmbeddedImageAnnotFromBase64(base64, x, y, width, height)
|
||||
End Sub
|
||||
|
||||
Private Sub AddImageAnnotation(pAnnotation As Annotation, pAttachments As Dictionary(Of String, Attachment))
|
||||
Dim oAttachment = pAttachments.Where(Function(a) a.Key = pAnnotation.imageAttachmentId).
|
||||
SingleOrDefault()
|
||||
@@ -137,6 +208,23 @@ Namespace Jobs.FinalizeDocument
|
||||
Manager.AddEmbeddedImageAnnotFromBase64(oAttachment.Value.binary, oX, oY, oWidth, oHeight)
|
||||
End Sub
|
||||
|
||||
Private Sub AddInkAnnotation(page As Integer, value As String)
|
||||
|
||||
Dim ink = JsonConvert.DeserializeObject(Of Ink)(value)
|
||||
|
||||
Dim oSegments = ink.lines.points
|
||||
Dim oColor = ColorTranslator.FromHtml(ink.strokeColor)
|
||||
Manager.SelectPage(page)
|
||||
|
||||
For Each oSegment As List(Of List(Of Single)) In oSegments
|
||||
Dim oPoints = oSegment.
|
||||
Select(AddressOf ToPointF).
|
||||
ToArray()
|
||||
|
||||
Manager.AddFreeHandAnnot(oColor, oPoints)
|
||||
Next
|
||||
End Sub
|
||||
|
||||
Private Sub AddInkAnnotation(pAnnotation As Annotation)
|
||||
Dim oSegments = pAnnotation.lines.points
|
||||
Dim oColor = ColorTranslator.FromHtml(pAnnotation.strokeColor)
|
||||
@@ -151,6 +239,19 @@ Namespace Jobs.FinalizeDocument
|
||||
Next
|
||||
End Sub
|
||||
|
||||
Private Sub AddFormFieldValue(x As Double, y As Double, width As Double, height As Double, page As Integer, value As String)
|
||||
Manager.SelectPage(page)
|
||||
|
||||
' Add the text annotation
|
||||
Dim ant = Manager.AddTextAnnot(x, y, width, height, value)
|
||||
|
||||
' Set the font properties
|
||||
ant.FontName = _pdfBurnerParams.FontName
|
||||
ant.FontSize = _pdfBurnerParams.FontSize
|
||||
ant.FontStyle = _pdfBurnerParams.FontStyle
|
||||
Manager.SaveAnnotationsToPage()
|
||||
End Sub
|
||||
|
||||
Private Sub AddFormFieldValue(pAnnotation As Annotation, formFieldValue As FormFieldValue)
|
||||
Dim ffIndex As Integer = EGName.Index(pAnnotation.egName)
|
||||
|
||||
@@ -194,6 +295,7 @@ Namespace Jobs.FinalizeDocument
|
||||
Public Const Image As String = "pspdfkit/image"
|
||||
Public Const Ink As String = "pspdfkit/ink"
|
||||
Public Const Widget As String = "pspdfkit/widget"
|
||||
Public Const FormField As String = "pspdfkit/form-field-value"
|
||||
End Class
|
||||
|
||||
Friend Class AnnotationData
|
||||
@@ -297,6 +399,12 @@ Namespace Jobs.FinalizeDocument
|
||||
Public Property strokeColor As String
|
||||
End Class
|
||||
|
||||
Friend Class Ink
|
||||
Public Property lines As Lines
|
||||
|
||||
Public Property strokeColor As String
|
||||
End Class
|
||||
|
||||
Public Class EGName
|
||||
Public Shared ReadOnly NoName As String = Guid.NewGuid().ToString()
|
||||
|
||||
@@ -327,4 +435,4 @@ Namespace Jobs.FinalizeDocument
|
||||
End Class
|
||||
#End Region
|
||||
End Class
|
||||
End Namespace
|
||||
End Namespace
|
||||
@@ -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.Core.Abstraction.Application" version="1.4.0" targetFramework="net462" />
|
||||
<package id="DigitalData.Core.Abstractions" version="4.2.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" />
|
||||
<package id="DigitalData.Modules.Database" version="2.3.5.4" targetFramework="net462" />
|
||||
|
||||
@@ -36,6 +36,34 @@ public class ElementAnnotation
|
||||
[Column("TYPE", TypeName = "nvarchar(50)")]
|
||||
public string Type { get; set; }
|
||||
|
||||
[Column("POSITION_X", TypeName = "float")]
|
||||
public double
|
||||
#if NET
|
||||
?
|
||||
#endif
|
||||
X { get; set; }
|
||||
|
||||
[Column("POSITION_Y", TypeName = "float")]
|
||||
public double
|
||||
#if NET
|
||||
?
|
||||
#endif
|
||||
Y { get; set; }
|
||||
|
||||
[Column("WIDTH", TypeName = "float")]
|
||||
public double
|
||||
#if NET
|
||||
?
|
||||
#endif
|
||||
Width { get; set; }
|
||||
|
||||
[Column("HEIGHT", TypeName = "float")]
|
||||
public double
|
||||
#if NET
|
||||
?
|
||||
#endif
|
||||
Height { get; set; }
|
||||
|
||||
[Required]
|
||||
[Column("ADDED_WHEN", TypeName = "datetime")]
|
||||
public DateTime AddedWhen { get; set; }
|
||||
@@ -52,10 +80,10 @@ public class ElementAnnotation
|
||||
ChangedWho { get; set; }
|
||||
|
||||
[ForeignKey("ElementId")]
|
||||
public virtual Signature
|
||||
public virtual Signature
|
||||
#if NET
|
||||
?
|
||||
#endif
|
||||
#endif
|
||||
Element { get; set; }
|
||||
}
|
||||
|
||||
|
||||
@@ -37,7 +37,7 @@
|
||||
<PackageReference Include="DigitalData.EmailProfilerDispatcher.Abstraction.Attributes" Version="1.0.0" />
|
||||
<PackageReference Include="Newtonsoft.Json" Version="13.0.3" />
|
||||
<PackageReference Include="UserManager.Domain" Version="3.2.3" />
|
||||
<PackageReference Include="DigitalData.Core.Abstractions" Version="4.2.0" />
|
||||
<PackageReference Include="DigitalData.Core.Abstractions" Version="4.3.0" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
|
||||
@@ -2,6 +2,8 @@
|
||||
using iText.Kernel.Pdf.Canvas;
|
||||
using EnvelopeGenerator.Domain.Interfaces;
|
||||
using iText.Kernel.Colors;
|
||||
using DigitalData.Core.Abstractions.Interfaces;
|
||||
|
||||
#if NETFRAMEWORK
|
||||
using System;
|
||||
using System.IO;
|
||||
@@ -100,7 +102,7 @@ namespace EnvelopeGenerator.PdfEditor
|
||||
});
|
||||
#endregion
|
||||
|
||||
public Pdf<TInputStream, TOutputStream> Background<TSignature>(IEnumerable<TSignature> signatures)
|
||||
public Pdf<TInputStream, TOutputStream> Background<TSignature>(IEnumerable<TSignature> signatures, double widthPx = 1.9500000000000002, double heightPx = 2.52)
|
||||
where TSignature : ISignature
|
||||
{
|
||||
// once per page
|
||||
@@ -118,8 +120,8 @@ namespace EnvelopeGenerator.PdfEditor
|
||||
double magin = .2;
|
||||
double x = (signature.X - .7 - magin) * inchFactor;
|
||||
double y = (signature.Y - .5 - magin) * inchFactor;
|
||||
double width = 1.9500000000000002 * inchFactor;
|
||||
double height = 2.52 * inchFactor;
|
||||
double width = widthPx * inchFactor;
|
||||
double height = heightPx * inchFactor;
|
||||
|
||||
double bottomLineLength = 2.5;
|
||||
|
||||
|
||||
20
EnvelopeGenerator.ReceiverUI/Components/App.razor
Normal file
20
EnvelopeGenerator.ReceiverUI/Components/App.razor
Normal file
@@ -0,0 +1,20 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<base href="/" />
|
||||
<link rel="stylesheet" href="bootstrap/bootstrap.min.css" />
|
||||
<link rel="stylesheet" href="app.css" />
|
||||
<link rel="stylesheet" href="EnvelopeGenerator.ReceiverUI.styles.css" />
|
||||
<link rel="icon" type="image/png" href="favicon.png" />
|
||||
<HeadOutlet />
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<Routes />
|
||||
<script src="_framework/blazor.web.js"></script>
|
||||
</body>
|
||||
|
||||
</html>
|
||||
@@ -0,0 +1,23 @@
|
||||
@inherits LayoutComponentBase
|
||||
|
||||
<div class="page">
|
||||
<div class="sidebar">
|
||||
<NavMenu />
|
||||
</div>
|
||||
|
||||
<main>
|
||||
<div class="top-row px-4">
|
||||
<a href="https://learn.microsoft.com/aspnet/core/" target="_blank">About</a>
|
||||
</div>
|
||||
|
||||
<article class="content px-4">
|
||||
@Body
|
||||
</article>
|
||||
</main>
|
||||
</div>
|
||||
|
||||
<div id="blazor-error-ui">
|
||||
An unhandled error has occurred.
|
||||
<a href="" class="reload">Reload</a>
|
||||
<a class="dismiss">🗙</a>
|
||||
</div>
|
||||
@@ -0,0 +1,96 @@
|
||||
.page {
|
||||
position: relative;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
main {
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.sidebar {
|
||||
background-image: linear-gradient(180deg, rgb(5, 39, 103) 0%, #3a0647 70%);
|
||||
}
|
||||
|
||||
.top-row {
|
||||
background-color: #f7f7f7;
|
||||
border-bottom: 1px solid #d6d5d5;
|
||||
justify-content: flex-end;
|
||||
height: 3.5rem;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.top-row ::deep a, .top-row ::deep .btn-link {
|
||||
white-space: nowrap;
|
||||
margin-left: 1.5rem;
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
.top-row ::deep a:hover, .top-row ::deep .btn-link:hover {
|
||||
text-decoration: underline;
|
||||
}
|
||||
|
||||
.top-row ::deep a:first-child {
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
|
||||
@media (max-width: 640.98px) {
|
||||
.top-row {
|
||||
justify-content: space-between;
|
||||
}
|
||||
|
||||
.top-row ::deep a, .top-row ::deep .btn-link {
|
||||
margin-left: 0;
|
||||
}
|
||||
}
|
||||
|
||||
@media (min-width: 641px) {
|
||||
.page {
|
||||
flex-direction: row;
|
||||
}
|
||||
|
||||
.sidebar {
|
||||
width: 250px;
|
||||
height: 100vh;
|
||||
position: sticky;
|
||||
top: 0;
|
||||
}
|
||||
|
||||
.top-row {
|
||||
position: sticky;
|
||||
top: 0;
|
||||
z-index: 1;
|
||||
}
|
||||
|
||||
.top-row.auth ::deep a:first-child {
|
||||
flex: 1;
|
||||
text-align: right;
|
||||
width: 0;
|
||||
}
|
||||
|
||||
.top-row, article {
|
||||
padding-left: 2rem !important;
|
||||
padding-right: 1.5rem !important;
|
||||
}
|
||||
}
|
||||
|
||||
#blazor-error-ui {
|
||||
background: lightyellow;
|
||||
bottom: 0;
|
||||
box-shadow: 0 -1px 2px rgba(0, 0, 0, 0.2);
|
||||
display: none;
|
||||
left: 0;
|
||||
padding: 0.6rem 1.25rem 0.7rem 1.25rem;
|
||||
position: fixed;
|
||||
width: 100%;
|
||||
z-index: 1000;
|
||||
}
|
||||
|
||||
#blazor-error-ui .dismiss {
|
||||
cursor: pointer;
|
||||
position: absolute;
|
||||
right: 0.75rem;
|
||||
top: 0.5rem;
|
||||
}
|
||||
30
EnvelopeGenerator.ReceiverUI/Components/Layout/NavMenu.razor
Normal file
30
EnvelopeGenerator.ReceiverUI/Components/Layout/NavMenu.razor
Normal file
@@ -0,0 +1,30 @@
|
||||
<div class="top-row ps-3 navbar navbar-dark">
|
||||
<div class="container-fluid">
|
||||
<a class="navbar-brand" href="">EnvelopeGenerator.ReceiverUI</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<input type="checkbox" title="Navigation menu" class="navbar-toggler" />
|
||||
|
||||
<div class="nav-scrollable" onclick="document.querySelector('.navbar-toggler').click()">
|
||||
<nav class="flex-column">
|
||||
<div class="nav-item px-3">
|
||||
<NavLink class="nav-link" href="" Match="NavLinkMatch.All">
|
||||
<span class="bi bi-house-door-fill-nav-menu" aria-hidden="true"></span> Home
|
||||
</NavLink>
|
||||
</div>
|
||||
|
||||
<div class="nav-item px-3">
|
||||
<NavLink class="nav-link" href="counter">
|
||||
<span class="bi bi-plus-square-fill-nav-menu" aria-hidden="true"></span> Counter
|
||||
</NavLink>
|
||||
</div>
|
||||
|
||||
<div class="nav-item px-3">
|
||||
<NavLink class="nav-link" href="weather">
|
||||
<span class="bi bi-list-nested-nav-menu" aria-hidden="true"></span> Weather
|
||||
</NavLink>
|
||||
</div>
|
||||
</nav>
|
||||
</div>
|
||||
|
||||
105
EnvelopeGenerator.ReceiverUI/Components/Layout/NavMenu.razor.css
Normal file
105
EnvelopeGenerator.ReceiverUI/Components/Layout/NavMenu.razor.css
Normal file
@@ -0,0 +1,105 @@
|
||||
.navbar-toggler {
|
||||
appearance: none;
|
||||
cursor: pointer;
|
||||
width: 3.5rem;
|
||||
height: 2.5rem;
|
||||
color: white;
|
||||
position: absolute;
|
||||
top: 0.5rem;
|
||||
right: 1rem;
|
||||
border: 1px solid rgba(255, 255, 255, 0.1);
|
||||
background: url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 30 30'%3e%3cpath stroke='rgba%28255, 255, 255, 0.55%29' stroke-linecap='round' stroke-miterlimit='10' stroke-width='2' d='M4 7h22M4 15h22M4 23h22'/%3e%3c/svg%3e") no-repeat center/1.75rem rgba(255, 255, 255, 0.1);
|
||||
}
|
||||
|
||||
.navbar-toggler:checked {
|
||||
background-color: rgba(255, 255, 255, 0.5);
|
||||
}
|
||||
|
||||
.top-row {
|
||||
height: 3.5rem;
|
||||
background-color: rgba(0,0,0,0.4);
|
||||
}
|
||||
|
||||
.navbar-brand {
|
||||
font-size: 1.1rem;
|
||||
}
|
||||
|
||||
.bi {
|
||||
display: inline-block;
|
||||
position: relative;
|
||||
width: 1.25rem;
|
||||
height: 1.25rem;
|
||||
margin-right: 0.75rem;
|
||||
top: -1px;
|
||||
background-size: cover;
|
||||
}
|
||||
|
||||
.bi-house-door-fill-nav-menu {
|
||||
background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='16' height='16' fill='white' class='bi bi-house-door-fill' viewBox='0 0 16 16'%3E%3Cpath d='M6.5 14.5v-3.505c0-.245.25-.495.5-.495h2c.25 0 .5.25.5.5v3.5a.5.5 0 0 0 .5.5h4a.5.5 0 0 0 .5-.5v-7a.5.5 0 0 0-.146-.354L13 5.793V2.5a.5.5 0 0 0-.5-.5h-1a.5.5 0 0 0-.5.5v1.293L8.354 1.146a.5.5 0 0 0-.708 0l-6 6A.5.5 0 0 0 1.5 7.5v7a.5.5 0 0 0 .5.5h4a.5.5 0 0 0 .5-.5Z'/%3E%3C/svg%3E");
|
||||
}
|
||||
|
||||
.bi-plus-square-fill-nav-menu {
|
||||
background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='16' height='16' fill='white' class='bi bi-plus-square-fill' viewBox='0 0 16 16'%3E%3Cpath d='M2 0a2 2 0 0 0-2 2v12a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V2a2 2 0 0 0-2-2H2zm6.5 4.5v3h3a.5.5 0 0 1 0 1h-3v3a.5.5 0 0 1-1 0v-3h-3a.5.5 0 0 1 0-1h3v-3a.5.5 0 0 1 1 0z'/%3E%3C/svg%3E");
|
||||
}
|
||||
|
||||
.bi-list-nested-nav-menu {
|
||||
background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='16' height='16' fill='white' class='bi bi-list-nested' viewBox='0 0 16 16'%3E%3Cpath fill-rule='evenodd' d='M4.5 11.5A.5.5 0 0 1 5 11h10a.5.5 0 0 1 0 1H5a.5.5 0 0 1-.5-.5zm-2-4A.5.5 0 0 1 3 7h10a.5.5 0 0 1 0 1H3a.5.5 0 0 1-.5-.5zm-2-4A.5.5 0 0 1 1 3h10a.5.5 0 0 1 0 1H1a.5.5 0 0 1-.5-.5z'/%3E%3C/svg%3E");
|
||||
}
|
||||
|
||||
.nav-item {
|
||||
font-size: 0.9rem;
|
||||
padding-bottom: 0.5rem;
|
||||
}
|
||||
|
||||
.nav-item:first-of-type {
|
||||
padding-top: 1rem;
|
||||
}
|
||||
|
||||
.nav-item:last-of-type {
|
||||
padding-bottom: 1rem;
|
||||
}
|
||||
|
||||
.nav-item ::deep .nav-link {
|
||||
color: #d7d7d7;
|
||||
background: none;
|
||||
border: none;
|
||||
border-radius: 4px;
|
||||
height: 3rem;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
line-height: 3rem;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.nav-item ::deep a.active {
|
||||
background-color: rgba(255,255,255,0.37);
|
||||
color: white;
|
||||
}
|
||||
|
||||
.nav-item ::deep .nav-link:hover {
|
||||
background-color: rgba(255,255,255,0.1);
|
||||
color: white;
|
||||
}
|
||||
|
||||
.nav-scrollable {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.navbar-toggler:checked ~ .nav-scrollable {
|
||||
display: block;
|
||||
}
|
||||
|
||||
@media (min-width: 641px) {
|
||||
.navbar-toggler {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.nav-scrollable {
|
||||
/* Never collapse the sidebar for wide screens */
|
||||
display: block;
|
||||
|
||||
/* Allow sidebar to scroll for tall menus */
|
||||
height: calc(100vh - 3.5rem);
|
||||
overflow-y: auto;
|
||||
}
|
||||
}
|
||||
19
EnvelopeGenerator.ReceiverUI/Components/Pages/Counter.razor
Normal file
19
EnvelopeGenerator.ReceiverUI/Components/Pages/Counter.razor
Normal file
@@ -0,0 +1,19 @@
|
||||
@page "/counter"
|
||||
@rendermode InteractiveServer
|
||||
|
||||
<PageTitle>Counter</PageTitle>
|
||||
|
||||
<h1>Counter</h1>
|
||||
|
||||
<p role="status">Current count: @currentCount</p>
|
||||
|
||||
<button class="btn btn-primary" @onclick="IncrementCount">Click me</button>
|
||||
|
||||
@code {
|
||||
private int currentCount = 0;
|
||||
|
||||
private void IncrementCount()
|
||||
{
|
||||
currentCount++;
|
||||
}
|
||||
}
|
||||
36
EnvelopeGenerator.ReceiverUI/Components/Pages/Error.razor
Normal file
36
EnvelopeGenerator.ReceiverUI/Components/Pages/Error.razor
Normal file
@@ -0,0 +1,36 @@
|
||||
@page "/Error"
|
||||
@using System.Diagnostics
|
||||
|
||||
<PageTitle>Error</PageTitle>
|
||||
|
||||
<h1 class="text-danger">Error.</h1>
|
||||
<h2 class="text-danger">An error occurred while processing your request.</h2>
|
||||
|
||||
@if (ShowRequestId)
|
||||
{
|
||||
<p>
|
||||
<strong>Request ID:</strong> <code>@RequestId</code>
|
||||
</p>
|
||||
}
|
||||
|
||||
<h3>Development Mode</h3>
|
||||
<p>
|
||||
Swapping to <strong>Development</strong> environment will display more detailed information about the error that occurred.
|
||||
</p>
|
||||
<p>
|
||||
<strong>The Development environment shouldn't be enabled for deployed applications.</strong>
|
||||
It can result in displaying sensitive information from exceptions to end users.
|
||||
For local debugging, enable the <strong>Development</strong> environment by setting the <strong>ASPNETCORE_ENVIRONMENT</strong> environment variable to <strong>Development</strong>
|
||||
and restarting the app.
|
||||
</p>
|
||||
|
||||
@code{
|
||||
[CascadingParameter]
|
||||
private HttpContext? HttpContext { get; set; }
|
||||
|
||||
private string? RequestId { get; set; }
|
||||
private bool ShowRequestId => !string.IsNullOrEmpty(RequestId);
|
||||
|
||||
protected override void OnInitialized() =>
|
||||
RequestId = Activity.Current?.Id ?? HttpContext?.TraceIdentifier;
|
||||
}
|
||||
7
EnvelopeGenerator.ReceiverUI/Components/Pages/Home.razor
Normal file
7
EnvelopeGenerator.ReceiverUI/Components/Pages/Home.razor
Normal file
@@ -0,0 +1,7 @@
|
||||
@page "/"
|
||||
|
||||
<PageTitle>Home</PageTitle>
|
||||
|
||||
<h1>Hello, world!</h1>
|
||||
|
||||
Welcome to your new app.
|
||||
64
EnvelopeGenerator.ReceiverUI/Components/Pages/Weather.razor
Normal file
64
EnvelopeGenerator.ReceiverUI/Components/Pages/Weather.razor
Normal file
@@ -0,0 +1,64 @@
|
||||
@page "/weather"
|
||||
@attribute [StreamRendering]
|
||||
|
||||
<PageTitle>Weather</PageTitle>
|
||||
|
||||
<h1>Weather</h1>
|
||||
|
||||
<p>This component demonstrates showing data.</p>
|
||||
|
||||
@if (forecasts == null)
|
||||
{
|
||||
<p><em>Loading...</em></p>
|
||||
}
|
||||
else
|
||||
{
|
||||
<table class="table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Date</th>
|
||||
<th>Temp. (C)</th>
|
||||
<th>Temp. (F)</th>
|
||||
<th>Summary</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
@foreach (var forecast in forecasts)
|
||||
{
|
||||
<tr>
|
||||
<td>@forecast.Date.ToShortDateString()</td>
|
||||
<td>@forecast.TemperatureC</td>
|
||||
<td>@forecast.TemperatureF</td>
|
||||
<td>@forecast.Summary</td>
|
||||
</tr>
|
||||
}
|
||||
</tbody>
|
||||
</table>
|
||||
}
|
||||
|
||||
@code {
|
||||
private WeatherForecast[]? forecasts;
|
||||
|
||||
protected override async Task OnInitializedAsync()
|
||||
{
|
||||
// Simulate asynchronous loading to demonstrate streaming rendering
|
||||
await Task.Delay(500);
|
||||
|
||||
var startDate = DateOnly.FromDateTime(DateTime.Now);
|
||||
var summaries = new[] { "Freezing", "Bracing", "Chilly", "Cool", "Mild", "Warm", "Balmy", "Hot", "Sweltering", "Scorching" };
|
||||
forecasts = Enumerable.Range(1, 5).Select(index => new WeatherForecast
|
||||
{
|
||||
Date = startDate.AddDays(index),
|
||||
TemperatureC = Random.Shared.Next(-20, 55),
|
||||
Summary = summaries[Random.Shared.Next(summaries.Length)]
|
||||
}).ToArray();
|
||||
}
|
||||
|
||||
private class WeatherForecast
|
||||
{
|
||||
public DateOnly Date { get; set; }
|
||||
public int TemperatureC { get; set; }
|
||||
public string? Summary { get; set; }
|
||||
public int TemperatureF => 32 + (int)(TemperatureC / 0.5556);
|
||||
}
|
||||
}
|
||||
6
EnvelopeGenerator.ReceiverUI/Components/Routes.razor
Normal file
6
EnvelopeGenerator.ReceiverUI/Components/Routes.razor
Normal file
@@ -0,0 +1,6 @@
|
||||
<Router AppAssembly="typeof(Program).Assembly">
|
||||
<Found Context="routeData">
|
||||
<RouteView RouteData="routeData" DefaultLayout="typeof(Layout.MainLayout)" />
|
||||
<FocusOnNavigate RouteData="routeData" Selector="h1" />
|
||||
</Found>
|
||||
</Router>
|
||||
10
EnvelopeGenerator.ReceiverUI/Components/_Imports.razor
Normal file
10
EnvelopeGenerator.ReceiverUI/Components/_Imports.razor
Normal file
@@ -0,0 +1,10 @@
|
||||
@using System.Net.Http
|
||||
@using System.Net.Http.Json
|
||||
@using Microsoft.AspNetCore.Components.Forms
|
||||
@using Microsoft.AspNetCore.Components.Routing
|
||||
@using Microsoft.AspNetCore.Components.Web
|
||||
@using static Microsoft.AspNetCore.Components.Web.RenderMode
|
||||
@using Microsoft.AspNetCore.Components.Web.Virtualization
|
||||
@using Microsoft.JSInterop
|
||||
@using EnvelopeGenerator.ReceiverUI
|
||||
@using EnvelopeGenerator.ReceiverUI.Components
|
||||
@@ -0,0 +1,9 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk.Web">
|
||||
|
||||
<PropertyGroup>
|
||||
<TargetFramework>net8.0</TargetFramework>
|
||||
<Nullable>enable</Nullable>
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
</PropertyGroup>
|
||||
|
||||
</Project>
|
||||
27
EnvelopeGenerator.ReceiverUI/Program.cs
Normal file
27
EnvelopeGenerator.ReceiverUI/Program.cs
Normal file
@@ -0,0 +1,27 @@
|
||||
using EnvelopeGenerator.ReceiverUI.Components;
|
||||
|
||||
var builder = WebApplication.CreateBuilder(args);
|
||||
|
||||
// Add services to the container.
|
||||
builder.Services.AddRazorComponents()
|
||||
.AddInteractiveServerComponents();
|
||||
|
||||
var app = builder.Build();
|
||||
|
||||
// Configure the HTTP request pipeline.
|
||||
if (!app.Environment.IsDevelopment())
|
||||
{
|
||||
app.UseExceptionHandler("/Error", createScopeForErrors: true);
|
||||
// The default HSTS value is 30 days. You may want to change this for production scenarios, see https://aka.ms/aspnetcore-hsts.
|
||||
app.UseHsts();
|
||||
}
|
||||
|
||||
app.UseHttpsRedirection();
|
||||
|
||||
app.UseStaticFiles();
|
||||
app.UseAntiforgery();
|
||||
|
||||
app.MapRazorComponents<App>()
|
||||
.AddInteractiveServerRenderMode();
|
||||
|
||||
app.Run();
|
||||
38
EnvelopeGenerator.ReceiverUI/Properties/launchSettings.json
Normal file
38
EnvelopeGenerator.ReceiverUI/Properties/launchSettings.json
Normal file
@@ -0,0 +1,38 @@
|
||||
{
|
||||
"$schema": "http://json.schemastore.org/launchsettings.json",
|
||||
"iisSettings": {
|
||||
"windowsAuthentication": false,
|
||||
"anonymousAuthentication": true,
|
||||
"iisExpress": {
|
||||
"applicationUrl": "http://localhost:26087",
|
||||
"sslPort": 44331
|
||||
}
|
||||
},
|
||||
"profiles": {
|
||||
"http": {
|
||||
"commandName": "Project",
|
||||
"dotnetRunMessages": true,
|
||||
"launchBrowser": true,
|
||||
"applicationUrl": "http://localhost:5134",
|
||||
"environmentVariables": {
|
||||
"ASPNETCORE_ENVIRONMENT": "Development"
|
||||
}
|
||||
},
|
||||
"https": {
|
||||
"commandName": "Project",
|
||||
"dotnetRunMessages": true,
|
||||
"launchBrowser": true,
|
||||
"applicationUrl": "https://localhost:7124;http://localhost:5134",
|
||||
"environmentVariables": {
|
||||
"ASPNETCORE_ENVIRONMENT": "Development"
|
||||
}
|
||||
},
|
||||
"IIS Express": {
|
||||
"commandName": "IISExpress",
|
||||
"launchBrowser": true,
|
||||
"environmentVariables": {
|
||||
"ASPNETCORE_ENVIRONMENT": "Development"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,8 @@
|
||||
{
|
||||
"Logging": {
|
||||
"LogLevel": {
|
||||
"Default": "Information",
|
||||
"Microsoft.AspNetCore": "Warning"
|
||||
}
|
||||
}
|
||||
}
|
||||
9
EnvelopeGenerator.ReceiverUI/appsettings.json
Normal file
9
EnvelopeGenerator.ReceiverUI/appsettings.json
Normal file
@@ -0,0 +1,9 @@
|
||||
{
|
||||
"Logging": {
|
||||
"LogLevel": {
|
||||
"Default": "Information",
|
||||
"Microsoft.AspNetCore": "Warning"
|
||||
}
|
||||
},
|
||||
"AllowedHosts": "*"
|
||||
}
|
||||
51
EnvelopeGenerator.ReceiverUI/wwwroot/app.css
Normal file
51
EnvelopeGenerator.ReceiverUI/wwwroot/app.css
Normal file
@@ -0,0 +1,51 @@
|
||||
html, body {
|
||||
font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif;
|
||||
}
|
||||
|
||||
a, .btn-link {
|
||||
color: #006bb7;
|
||||
}
|
||||
|
||||
.btn-primary {
|
||||
color: #fff;
|
||||
background-color: #1b6ec2;
|
||||
border-color: #1861ac;
|
||||
}
|
||||
|
||||
.btn:focus, .btn:active:focus, .btn-link.nav-link:focus, .form-control:focus, .form-check-input:focus {
|
||||
box-shadow: 0 0 0 0.1rem white, 0 0 0 0.25rem #258cfb;
|
||||
}
|
||||
|
||||
.content {
|
||||
padding-top: 1.1rem;
|
||||
}
|
||||
|
||||
h1:focus {
|
||||
outline: none;
|
||||
}
|
||||
|
||||
.valid.modified:not([type=checkbox]) {
|
||||
outline: 1px solid #26b050;
|
||||
}
|
||||
|
||||
.invalid {
|
||||
outline: 1px solid #e50000;
|
||||
}
|
||||
|
||||
.validation-message {
|
||||
color: #e50000;
|
||||
}
|
||||
|
||||
.blazor-error-boundary {
|
||||
background: url(data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iNTYiIGhlaWdodD0iNDkiIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyIgeG1sbnM6eGxpbms9Imh0dHA6Ly93d3cudzMub3JnLzE5OTkveGxpbmsiIG92ZXJmbG93PSJoaWRkZW4iPjxkZWZzPjxjbGlwUGF0aCBpZD0iY2xpcDAiPjxyZWN0IHg9IjIzNSIgeT0iNTEiIHdpZHRoPSI1NiIgaGVpZ2h0PSI0OSIvPjwvY2xpcFBhdGg+PC9kZWZzPjxnIGNsaXAtcGF0aD0idXJsKCNjbGlwMCkiIHRyYW5zZm9ybT0idHJhbnNsYXRlKC0yMzUgLTUxKSI+PHBhdGggZD0iTTI2My41MDYgNTFDMjY0LjcxNyA1MSAyNjUuODEzIDUxLjQ4MzcgMjY2LjYwNiA1Mi4yNjU4TDI2Ny4wNTIgNTIuNzk4NyAyNjcuNTM5IDUzLjYyODMgMjkwLjE4NSA5Mi4xODMxIDI5MC41NDUgOTIuNzk1IDI5MC42NTYgOTIuOTk2QzI5MC44NzcgOTMuNTEzIDI5MSA5NC4wODE1IDI5MSA5NC42NzgyIDI5MSA5Ny4wNjUxIDI4OS4wMzggOTkgMjg2LjYxNyA5OUwyNDAuMzgzIDk5QzIzNy45NjMgOTkgMjM2IDk3LjA2NTEgMjM2IDk0LjY3ODIgMjM2IDk0LjM3OTkgMjM2LjAzMSA5NC4wODg2IDIzNi4wODkgOTMuODA3MkwyMzYuMzM4IDkzLjAxNjIgMjM2Ljg1OCA5Mi4xMzE0IDI1OS40NzMgNTMuNjI5NCAyNTkuOTYxIDUyLjc5ODUgMjYwLjQwNyA1Mi4yNjU4QzI2MS4yIDUxLjQ4MzcgMjYyLjI5NiA1MSAyNjMuNTA2IDUxWk0yNjMuNTg2IDY2LjAxODNDMjYwLjczNyA2Ni4wMTgzIDI1OS4zMTMgNjcuMTI0NSAyNTkuMzEzIDY5LjMzNyAyNTkuMzEzIDY5LjYxMDIgMjU5LjMzMiA2OS44NjA4IDI1OS4zNzEgNzAuMDg4N0wyNjEuNzk1IDg0LjAxNjEgMjY1LjM4IDg0LjAxNjEgMjY3LjgyMSA2OS43NDc1QzI2Ny44NiA2OS43MzA5IDI2Ny44NzkgNjkuNTg3NyAyNjcuODc5IDY5LjMxNzkgMjY3Ljg3OSA2Ny4xMTgyIDI2Ni40NDggNjYuMDE4MyAyNjMuNTg2IDY2LjAxODNaTTI2My41NzYgODYuMDU0N0MyNjEuMDQ5IDg2LjA1NDcgMjU5Ljc4NiA4Ny4zMDA1IDI1OS43ODYgODkuNzkyMSAyNTkuNzg2IDkyLjI4MzcgMjYxLjA0OSA5My41Mjk1IDI2My41NzYgOTMuNTI5NSAyNjYuMTE2IDkzLjUyOTUgMjY3LjM4NyA5Mi4yODM3IDI2Ny4zODcgODkuNzkyMSAyNjcuMzg3IDg3LjMwMDUgMjY2LjExNiA4Ni4wNTQ3IDI2My41NzYgODYuMDU0N1oiIGZpbGw9IiNGRkU1MDAiIGZpbGwtcnVsZT0iZXZlbm9kZCIvPjwvZz48L3N2Zz4=) no-repeat 1rem/1.8rem, #b32121;
|
||||
padding: 1rem 1rem 1rem 3.7rem;
|
||||
color: white;
|
||||
}
|
||||
|
||||
.blazor-error-boundary::after {
|
||||
content: "An error has occurred."
|
||||
}
|
||||
|
||||
.darker-border-checkbox.form-check-input {
|
||||
border-color: #929292;
|
||||
}
|
||||
7
EnvelopeGenerator.ReceiverUI/wwwroot/bootstrap/bootstrap.min.css
vendored
Normal file
7
EnvelopeGenerator.ReceiverUI/wwwroot/bootstrap/bootstrap.min.css
vendored
Normal file
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
BIN
EnvelopeGenerator.ReceiverUI/wwwroot/favicon.png
Normal file
BIN
EnvelopeGenerator.ReceiverUI/wwwroot/favicon.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 1.1 KiB |
13
EnvelopeGenerator.ReceiverUIBlazor/App.razor
Normal file
13
EnvelopeGenerator.ReceiverUIBlazor/App.razor
Normal file
@@ -0,0 +1,13 @@
|
||||
@using Microsoft.AspNetCore.Components.Routing
|
||||
|
||||
<Router AppAssembly="@typeof(App).Assembly">
|
||||
<Found Context="routeData">
|
||||
<RouteView RouteData="routeData" DefaultLayout="typeof(MainLayout)" />
|
||||
<FocusOnNavigate RouteData="routeData" Selector="h1" />
|
||||
</Found>
|
||||
<NotFound>
|
||||
<LayoutView Layout="typeof(MainLayout)">
|
||||
<p role="alert">Sorry, there's nothing at this address.</p>
|
||||
</LayoutView>
|
||||
</NotFound>
|
||||
</Router>
|
||||
@@ -0,0 +1,13 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk.BlazorWebAssembly">
|
||||
<PropertyGroup>
|
||||
<TargetFramework>net8.0</TargetFramework>
|
||||
<Nullable>enable</Nullable>
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
<AssemblyName>EnvelopeGenerator.ReceiverUIBlazor</AssemblyName>
|
||||
<RootNamespace>EnvelopeGenerator.ReceiverUIBlazor</RootNamespace>
|
||||
</PropertyGroup>
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Microsoft.AspNetCore.Components.WebAssembly" Version="8.0.0" />
|
||||
<PackageReference Include="Microsoft.AspNetCore.Components.WebAssembly.DevServer" Version="8.0.0" PrivateAssets="all" />
|
||||
</ItemGroup>
|
||||
</Project>
|
||||
428
EnvelopeGenerator.ReceiverUIBlazor/Pages/Index.razor
Normal file
428
EnvelopeGenerator.ReceiverUIBlazor/Pages/Index.razor
Normal file
@@ -0,0 +1,428 @@
|
||||
@page "/"
|
||||
@using Microsoft.JSInterop
|
||||
@inject IJSRuntime JS
|
||||
@implements IAsyncDisposable
|
||||
|
||||
<div class="page-shell">
|
||||
<h1>Sign PDF (Blazor)</h1>
|
||||
|
||||
<div class="controls">
|
||||
<InputFile OnChange="HandleFileSelected" accept="application/pdf" />
|
||||
<button class="btn" @onclick="ShowSignaturePad" disabled="@(!HasPdf)">Add signature</button>
|
||||
<button class="btn" @onclick="() => ShowTextOverlay(false)" disabled="@(!HasPdf)">Add text</button>
|
||||
<button class="btn" @onclick="() => ShowTextOverlay(true)" disabled="@(!HasPdf)">Add date</button>
|
||||
<button class="btn" @onclick="Reset" disabled="@(!HasPdf)">Reset</button>
|
||||
<button class="btn secondary" @onclick="Download" disabled="@(!HasPdf)">Download</button>
|
||||
</div>
|
||||
|
||||
@if (!string.IsNullOrWhiteSpace(ErrorMessage))
|
||||
{
|
||||
<div class="error-banner">@ErrorMessage</div>
|
||||
}
|
||||
|
||||
@if (!HasPdf)
|
||||
{
|
||||
<div class="drop-hint">Drop or select a PDF to start.</div>
|
||||
}
|
||||
|
||||
@if (HasPdf)
|
||||
{
|
||||
<div class="document-shell" @ref="PdfHostRef" style="@($"width:{ViewportWidthPx}px")">
|
||||
<canvas id="pdf-canvas" @ref="PdfCanvasRef"></canvas>
|
||||
|
||||
@if (ShowSignature)
|
||||
{
|
||||
<div class="overlay signature" @ref="OverlayRef"
|
||||
style="@($"left:{OverlayXpx}px; top:{OverlayYpx}px; width:{OverlayWidthPx}px; height:{OverlayHeightPx}px;")"
|
||||
@onpointerdown="StartDrag" @onpointermove="OnDrag" @onpointerup="EndDrag" @onpointercancel="EndDrag">
|
||||
<div class="overlay-controls">
|
||||
<button class="overlay-btn" @onclick="ApplySignature" @onpointerdown:stopPropagation @onpointerup:stopPropagation @onclick:stopPropagation>✔</button>
|
||||
<button class="overlay-btn" @onclick="CancelOverlay" @onpointerdown:stopPropagation @onpointerup:stopPropagation @onclick:stopPropagation>✖</button>
|
||||
</div>
|
||||
<img src="@SignatureDataUrl" draggable="false" />
|
||||
</div>
|
||||
}
|
||||
|
||||
@if (ShowText)
|
||||
{
|
||||
<div class="overlay text" @ref="OverlayRef"
|
||||
style="@($"left:{OverlayXpx}px; top:{OverlayYpx}px;")"
|
||||
@onpointerdown="StartDrag" @onpointermove="OnDrag" @onpointerup="EndDrag" @onpointercancel="EndDrag">
|
||||
<div class="overlay-controls">
|
||||
<button class="overlay-btn" @onclick="ApplyText" @onpointerdown:stopPropagation @onpointerup:stopPropagation @onclick:stopPropagation>✔</button>
|
||||
<button class="overlay-btn" @onclick="CancelOverlay" @onpointerdown:stopPropagation @onpointerup:stopPropagation @onclick:stopPropagation>✖</button>
|
||||
</div>
|
||||
<input class="overlay-input" @bind="TextValue" @onpointerdown:stopPropagation @onpointerup:stopPropagation @onclick:stopPropagation />
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
|
||||
<div class="paging">
|
||||
<button class="btn" @onclick="PrevPage" disabled="@(!CanPrev)"><</button>
|
||||
<span>Page @DisplayPage / @PageCount</span>
|
||||
<button class="btn" @onclick="NextPage" disabled="@(!CanNext)">></button>
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
|
||||
@if (ShowSignaturePadModal)
|
||||
{
|
||||
<div class="modal-backdrop">
|
||||
<div class="modal">
|
||||
<h3>Add signature</h3>
|
||||
<canvas id="@SignatureCanvasId" width="700" height="220"></canvas>
|
||||
<div class="modal-row">
|
||||
<label><input type="checkbox" @bind="AutoDate" /> Auto date/time</label>
|
||||
<button class="btn" @onclick="ClearSignature">Clear</button>
|
||||
<span class="spacer"></span>
|
||||
<button class="btn" @onclick="ConfirmSignature">Use signature</button>
|
||||
<button class="btn secondary" @onclick="CloseSignaturePad">Cancel</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
|
||||
@code {
|
||||
private ElementReference PdfCanvasRef;
|
||||
private ElementReference PdfHostRef;
|
||||
private ElementReference OverlayRef;
|
||||
|
||||
private string? PdfBase64;
|
||||
private string? OriginalPdfBase64;
|
||||
private DotNetObjectReference<Index>? _dotNetRef;
|
||||
private int PageIndex;
|
||||
private int PageCount;
|
||||
private double ViewportWidthPx = 800;
|
||||
private double ViewportHeightPx;
|
||||
|
||||
private bool ShowSignaturePadModal;
|
||||
private bool ShowSignature;
|
||||
private bool ShowText;
|
||||
private string SignatureCanvasId { get; } = $"sig-{Guid.NewGuid():N}";
|
||||
private string? SignatureDataUrl;
|
||||
private bool AutoDate = true;
|
||||
|
||||
private double OverlayXpx = 20;
|
||||
private double OverlayYpx = 20;
|
||||
private double OverlayWidthPx = 200;
|
||||
private double OverlayHeightPx = 80;
|
||||
private bool IsDragging;
|
||||
private double DragStartX;
|
||||
private double DragStartY;
|
||||
private double StartLeft;
|
||||
private double StartTop;
|
||||
private string TextValue = "Text";
|
||||
private string? ErrorMessage;
|
||||
private DateTimeOffset _lastDragRender = DateTimeOffset.MinValue;
|
||||
|
||||
private bool HasPdf => !string.IsNullOrWhiteSpace(PdfBase64);
|
||||
private int DisplayPage => PageIndex + 1;
|
||||
private bool CanPrev => PageIndex > 0;
|
||||
private bool CanNext => PageIndex + 1 < PageCount;
|
||||
|
||||
protected override async Task OnAfterRenderAsync(bool firstRender)
|
||||
{
|
||||
if (firstRender)
|
||||
{
|
||||
await JS.InvokeVoidAsync("pdfInterop.ensureReady");
|
||||
_dotNetRef ??= DotNetObjectReference.Create(this);
|
||||
await JS.InvokeVoidAsync("pdfInterop.registerDropHandler", _dotNetRef);
|
||||
}
|
||||
|
||||
if (ShowSignaturePadModal)
|
||||
{
|
||||
await JS.InvokeVoidAsync("pdfInterop.initSignaturePad", SignatureCanvasId);
|
||||
}
|
||||
}
|
||||
|
||||
private async Task HandleFileSelected(InputFileChangeEventArgs e)
|
||||
{
|
||||
if (e.FileCount == 0)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
await LoadPdfFromBrowserFile(e.File);
|
||||
}
|
||||
|
||||
private async Task LoadPdfFromBrowserFile(IBrowserFile file)
|
||||
{
|
||||
ErrorMessage = null;
|
||||
|
||||
try
|
||||
{
|
||||
await using var stream = file.OpenReadStream(maxAllowedSize: 20 * 1024 * 1024);
|
||||
using var ms = new MemoryStream();
|
||||
await stream.CopyToAsync(ms);
|
||||
await LoadPdfFromBase64Internal(Convert.ToBase64String(ms.ToArray()));
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
ErrorMessage = $"Fehler beim Laden der PDF: {ex.Message}";
|
||||
PdfBase64 = null;
|
||||
PageCount = 0;
|
||||
PageIndex = 0;
|
||||
}
|
||||
}
|
||||
|
||||
[JSInvokable]
|
||||
public Task LoadPdfFromBase64(string base64)
|
||||
{
|
||||
return LoadPdfFromBase64Internal(base64);
|
||||
}
|
||||
|
||||
private async Task LoadPdfFromBase64Internal(string base64)
|
||||
{
|
||||
ErrorMessage = null;
|
||||
PdfBase64 = base64;
|
||||
OriginalPdfBase64 = PdfBase64;
|
||||
|
||||
// Show the canvas before we start rendering
|
||||
await InvokeAsync(StateHasChanged);
|
||||
await Task.Yield();
|
||||
|
||||
// Make sure pdf.js is ready
|
||||
await JS.InvokeVoidAsync("pdfInterop.ensureReady");
|
||||
|
||||
var result = await JS.InvokeAsync<RenderResult>("pdfInterop.loadPdf", PdfBase64);
|
||||
PageCount = result.Pages;
|
||||
PageIndex = 0;
|
||||
|
||||
await RenderPage();
|
||||
}
|
||||
|
||||
private async Task RenderPage()
|
||||
{
|
||||
if (!HasPdf)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
var viewport = await JS.InvokeAsync<ViewportInfo>("pdfInterop.renderPage", PageIndex, "pdf-canvas", ViewportWidthPx);
|
||||
ViewportWidthPx = viewport.Width;
|
||||
ViewportHeightPx = viewport.Height;
|
||||
StateHasChanged();
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
ErrorMessage = $"Fehler beim Rendern: {ex.Message}";
|
||||
}
|
||||
}
|
||||
|
||||
private async Task Reset()
|
||||
{
|
||||
ErrorMessage = null;
|
||||
CloseOverlays();
|
||||
ShowSignaturePadModal = false;
|
||||
OverlayXpx = 20;
|
||||
OverlayYpx = 20;
|
||||
OverlayWidthPx = 200;
|
||||
OverlayHeightPx = 80;
|
||||
TextValue = "Text";
|
||||
|
||||
if (string.IsNullOrWhiteSpace(OriginalPdfBase64))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
PdfBase64 = OriginalPdfBase64;
|
||||
PageIndex = 0;
|
||||
|
||||
var result = await JS.InvokeAsync<RenderResult>("pdfInterop.loadPdf", PdfBase64);
|
||||
PageCount = result.Pages;
|
||||
await RenderPage();
|
||||
}
|
||||
|
||||
private void CloseOverlays()
|
||||
{
|
||||
ShowSignature = false;
|
||||
ShowText = false;
|
||||
SignatureDataUrl = null;
|
||||
}
|
||||
|
||||
private void ShowSignaturePad()
|
||||
{
|
||||
ShowSignaturePadModal = true;
|
||||
}
|
||||
|
||||
private async Task ConfirmSignature()
|
||||
{
|
||||
SignatureDataUrl = await JS.InvokeAsync<string>("pdfInterop.getSignatureDataUrl", SignatureCanvasId);
|
||||
if (string.IsNullOrWhiteSpace(SignatureDataUrl))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
OverlayWidthPx = 200;
|
||||
OverlayHeightPx = 80;
|
||||
OverlayXpx = 20;
|
||||
OverlayYpx = 20;
|
||||
ShowSignature = true;
|
||||
ShowText = false;
|
||||
ShowSignaturePadModal = false;
|
||||
}
|
||||
|
||||
private void CloseSignaturePad()
|
||||
{
|
||||
ShowSignaturePadModal = false;
|
||||
}
|
||||
|
||||
private void ClearSignature()
|
||||
{
|
||||
JS.InvokeVoidAsync("pdfInterop.clearSignaturePad", SignatureCanvasId);
|
||||
}
|
||||
|
||||
private void ShowTextOverlay(bool autoDate)
|
||||
{
|
||||
TextValue = autoDate ? DateTimeOffset.Now.ToString("M/d/yyyy HH:mm:ss zzz") : "Text";
|
||||
OverlayWidthPx = 240;
|
||||
OverlayHeightPx = 40;
|
||||
OverlayXpx = 20;
|
||||
OverlayYpx = 20;
|
||||
ShowText = true;
|
||||
ShowSignature = false;
|
||||
}
|
||||
|
||||
private void StartDrag(PointerEventArgs args)
|
||||
{
|
||||
IsDragging = true;
|
||||
DragStartX = args.ClientX;
|
||||
DragStartY = args.ClientY;
|
||||
StartLeft = OverlayXpx;
|
||||
StartTop = OverlayYpx;
|
||||
|
||||
if (OverlayRef.Context != null)
|
||||
{
|
||||
JS.InvokeVoidAsync("pdfInterop.capturePointer", OverlayRef, args.PointerId);
|
||||
}
|
||||
}
|
||||
|
||||
private void OnDrag(PointerEventArgs args)
|
||||
{
|
||||
if (!IsDragging)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
var dx = args.ClientX - DragStartX;
|
||||
var dy = args.ClientY - DragStartY;
|
||||
OverlayXpx = StartLeft + dx;
|
||||
OverlayYpx = StartTop + dy;
|
||||
|
||||
var now = DateTimeOffset.UtcNow;
|
||||
if (now - _lastDragRender > TimeSpan.FromMilliseconds(16))
|
||||
{
|
||||
_lastDragRender = now;
|
||||
InvokeAsync(StateHasChanged);
|
||||
}
|
||||
}
|
||||
|
||||
private void EndDrag(PointerEventArgs args)
|
||||
{
|
||||
IsDragging = false;
|
||||
|
||||
if (OverlayRef.Context != null)
|
||||
{
|
||||
JS.InvokeVoidAsync("pdfInterop.releasePointer", OverlayRef, args.PointerId);
|
||||
}
|
||||
}
|
||||
|
||||
private async Task ApplySignature()
|
||||
{
|
||||
if (SignatureDataUrl is null || !HasPdf)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
PdfBase64 = await JS.InvokeAsync<string>("pdfInterop.applySignature", new
|
||||
{
|
||||
base64 = PdfBase64,
|
||||
pageIndex = PageIndex,
|
||||
left = OverlayXpx,
|
||||
top = OverlayYpx,
|
||||
width = OverlayWidthPx,
|
||||
height = OverlayHeightPx,
|
||||
renderWidth = ViewportWidthPx,
|
||||
renderHeight = ViewportHeightPx,
|
||||
dataUrl = SignatureDataUrl,
|
||||
autoDate = AutoDate,
|
||||
});
|
||||
|
||||
CloseOverlays();
|
||||
await RenderPage();
|
||||
}
|
||||
|
||||
private async Task ApplyText()
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(TextValue) || !HasPdf)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
PdfBase64 = await JS.InvokeAsync<string>("pdfInterop.applyText", new
|
||||
{
|
||||
base64 = PdfBase64,
|
||||
pageIndex = PageIndex,
|
||||
left = OverlayXpx,
|
||||
top = OverlayYpx,
|
||||
width = OverlayWidthPx,
|
||||
height = OverlayHeightPx,
|
||||
renderWidth = ViewportWidthPx,
|
||||
renderHeight = ViewportHeightPx,
|
||||
text = TextValue,
|
||||
fontSize = 20,
|
||||
});
|
||||
|
||||
CloseOverlays();
|
||||
await RenderPage();
|
||||
}
|
||||
|
||||
private void CancelOverlay()
|
||||
{
|
||||
CloseOverlays();
|
||||
}
|
||||
|
||||
private async Task PrevPage()
|
||||
{
|
||||
if (!CanPrev)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
PageIndex--;
|
||||
await RenderPage();
|
||||
}
|
||||
|
||||
private async Task NextPage()
|
||||
{
|
||||
if (!CanNext)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
PageIndex++;
|
||||
await RenderPage();
|
||||
}
|
||||
|
||||
private async Task Download()
|
||||
{
|
||||
if (!HasPdf)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
await JS.InvokeVoidAsync("pdfInterop.downloadPdf", PdfBase64, "document-signed.pdf");
|
||||
}
|
||||
|
||||
private record RenderResult(int Pages);
|
||||
|
||||
private record ViewportInfo(double Width, double Height, double PageWidth, double PageHeight);
|
||||
|
||||
public async ValueTask DisposeAsync()
|
||||
{
|
||||
_dotNetRef?.Dispose();
|
||||
await Task.CompletedTask;
|
||||
}
|
||||
}
|
||||
11
EnvelopeGenerator.ReceiverUIBlazor/Program.cs
Normal file
11
EnvelopeGenerator.ReceiverUIBlazor/Program.cs
Normal file
@@ -0,0 +1,11 @@
|
||||
using Microsoft.AspNetCore.Components.Web;
|
||||
using Microsoft.AspNetCore.Components.WebAssembly.Hosting;
|
||||
using EnvelopeGenerator.ReceiverUIBlazor;
|
||||
|
||||
var builder = WebAssemblyHostBuilder.CreateDefault(args);
|
||||
builder.RootComponents.Add<App>("#app");
|
||||
builder.RootComponents.Add<HeadOutlet>("head::after");
|
||||
|
||||
builder.Services.AddScoped(sp => new HttpClient { BaseAddress = new Uri(builder.HostEnvironment.BaseAddress) });
|
||||
|
||||
await builder.Build().RunAsync();
|
||||
@@ -0,0 +1,17 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<!-- https://go.microsoft.com/fwlink/?LinkID=208121. -->
|
||||
<Project>
|
||||
<PropertyGroup>
|
||||
<WebPublishMethod>Package</WebPublishMethod>
|
||||
<LastUsedBuildConfiguration>Release</LastUsedBuildConfiguration>
|
||||
<LastUsedPlatform>Any CPU</LastUsedPlatform>
|
||||
<SiteUrlToLaunchAfterPublish />
|
||||
<LaunchSiteAfterPublish>true</LaunchSiteAfterPublish>
|
||||
<ExcludeApp_Data>false</ExcludeApp_Data>
|
||||
<ProjectGuid>7f262ad4-53b1-42d3-9a5f-132cf50f150c</ProjectGuid>
|
||||
<DesktopBuildPackageLocation>E:\TekH\Visual Studio\src\EnvelopeGenerator.ReceiverUIBlazor\EnvelopeGenerator.ReceiverUIBlazor.zip</DesktopBuildPackageLocation>
|
||||
<PackageAsSingleFile>true</PackageAsSingleFile>
|
||||
<DeployIisAppPath>ReceiverUIBlazor</DeployIisAppPath>
|
||||
<_TargetId>IISWebDeployPackage</_TargetId>
|
||||
</PropertyGroup>
|
||||
</Project>
|
||||
@@ -0,0 +1,12 @@
|
||||
{
|
||||
"profiles": {
|
||||
"EnvelopeGenerator.ReceiverUIBlazor": {
|
||||
"commandName": "Project",
|
||||
"launchBrowser": true,
|
||||
"environmentVariables": {
|
||||
"ASPNETCORE_ENVIRONMENT": "Development"
|
||||
},
|
||||
"applicationUrl": "https://localhost:49582;http://localhost:49583"
|
||||
}
|
||||
}
|
||||
}
|
||||
10
EnvelopeGenerator.ReceiverUIBlazor/Shared/MainLayout.razor
Normal file
10
EnvelopeGenerator.ReceiverUIBlazor/Shared/MainLayout.razor
Normal file
@@ -0,0 +1,10 @@
|
||||
@inherits LayoutComponentBase
|
||||
|
||||
<div class="main-layout">
|
||||
<header class="top-bar">
|
||||
<div class="brand">Receiver UI (Blazor)</div>
|
||||
</header>
|
||||
<main class="content">
|
||||
@Body
|
||||
</main>
|
||||
</div>
|
||||
8
EnvelopeGenerator.ReceiverUIBlazor/_Imports.razor
Normal file
8
EnvelopeGenerator.ReceiverUIBlazor/_Imports.razor
Normal file
@@ -0,0 +1,8 @@
|
||||
@using System.Net.Http
|
||||
@using Microsoft.AspNetCore.Components
|
||||
@using Microsoft.AspNetCore.Components.Web
|
||||
@using Microsoft.AspNetCore.Components.Forms
|
||||
@using Microsoft.AspNetCore.Components.WebAssembly.Hosting
|
||||
@using Microsoft.JSInterop
|
||||
@using EnvelopeGenerator.ReceiverUIBlazor
|
||||
@using EnvelopeGenerator.ReceiverUIBlazor.Shared
|
||||
241
EnvelopeGenerator.ReceiverUIBlazor/wwwroot/css/app.css
Normal file
241
EnvelopeGenerator.ReceiverUIBlazor/wwwroot/css/app.css
Normal file
@@ -0,0 +1,241 @@
|
||||
@import url('https://fonts.googleapis.com/css2?family=Red+Hat+Text:wght@400;500;600;700&family=Teko:wght@500;600&display=swap');
|
||||
|
||||
:root {
|
||||
--bg: #f7f7f8;
|
||||
--bg-strong: #fff6f6;
|
||||
--text: #474747;
|
||||
--muted: #777777;
|
||||
--border: #e7e7e7;
|
||||
--shadow: 0 18px 55px rgba(20, 20, 20, 0.08);
|
||||
--card: #ffffff;
|
||||
--accent: #a52431;
|
||||
--accent-strong: #8d1e2a;
|
||||
--accent-soft: #f8e5e8;
|
||||
--highlight: #ffd62f;
|
||||
--danger: #a52431;
|
||||
}
|
||||
|
||||
body {
|
||||
margin: 0;
|
||||
font-family: "Red Hat Text", "Segoe UI", system-ui, -apple-system, sans-serif;
|
||||
background: radial-gradient(120% 120% at 6% 12%, var(--bg-strong) 0%, #fffdf7 45%, var(--bg) 85%);
|
||||
color: var(--text);
|
||||
line-height: 1.5;
|
||||
}
|
||||
|
||||
.main-layout {
|
||||
min-height: 100vh;
|
||||
}
|
||||
|
||||
.top-bar {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding: 14px 24px;
|
||||
background: var(--accent);
|
||||
border-bottom: 1px solid var(--accent-strong);
|
||||
box-shadow: 0 8px 24px rgba(0, 0, 0, 0.12);
|
||||
color: #ffd62f;
|
||||
}
|
||||
|
||||
.top-bar .brand {
|
||||
font-family: "Teko", "Red Hat Text", sans-serif;
|
||||
font-weight: 600;
|
||||
letter-spacing: 0.6px;
|
||||
color: #ffd62f;
|
||||
}
|
||||
|
||||
.content {
|
||||
padding: 28px 32px 40px;
|
||||
max-width: 1200px;
|
||||
margin: 0 auto;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 16px;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
h1 {
|
||||
margin: 0 0 10px;
|
||||
letter-spacing: 0.2px;
|
||||
font-family: "Teko", "Red Hat Text", sans-serif;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.controls {
|
||||
display: flex;
|
||||
gap: 10px;
|
||||
flex-wrap: wrap;
|
||||
align-items: center;
|
||||
margin-bottom: 14px;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.btn {
|
||||
background: #4a4a4a;
|
||||
color: #ffffff;
|
||||
border: 1px solid #404040;
|
||||
padding: 10px 15px;
|
||||
border-radius: 12px;
|
||||
font-weight: 700;
|
||||
cursor: pointer;
|
||||
transition: transform 120ms ease, box-shadow 120ms ease, background-color 120ms ease;
|
||||
box-shadow: 0 8px 16px rgba(0, 0, 0, 0.12);
|
||||
}
|
||||
|
||||
.btn:hover:not(:disabled) {
|
||||
transform: translateY(-1px);
|
||||
background: #3f3f3f;
|
||||
}
|
||||
|
||||
.btn:disabled {
|
||||
opacity: 0.45;
|
||||
cursor: not-allowed;
|
||||
box-shadow: none;
|
||||
}
|
||||
|
||||
.btn.secondary {
|
||||
background: #f4f4f4;
|
||||
color: #474747;
|
||||
border: 1px solid #d3d3d3;
|
||||
box-shadow: none;
|
||||
}
|
||||
|
||||
.btn.secondary:hover:not(:disabled) {
|
||||
background: #e9e9e9;
|
||||
transform: translateY(-1px);
|
||||
}
|
||||
|
||||
.drop-hint {
|
||||
padding: 26px;
|
||||
border: 1px dashed var(--border);
|
||||
border-radius: 14px;
|
||||
text-align: center;
|
||||
color: var(--muted);
|
||||
background: #ffffff;
|
||||
width: min(1100px, 100%);
|
||||
margin: 0 auto;
|
||||
box-shadow: 0 8px 18px rgba(165, 36, 49, 0.06);
|
||||
}
|
||||
|
||||
.error-banner {
|
||||
margin-top: 10px;
|
||||
padding: 12px 14px;
|
||||
border-radius: 10px;
|
||||
background: #fcebec;
|
||||
border: 1px solid #f3c6cd;
|
||||
color: var(--accent-strong);
|
||||
}
|
||||
|
||||
.document-shell {
|
||||
position: relative;
|
||||
margin-top: 14px;
|
||||
border-radius: 14px;
|
||||
overflow: hidden;
|
||||
box-shadow: var(--shadow);
|
||||
background: var(--card);
|
||||
border: 1px solid var(--border);
|
||||
margin-left: auto;
|
||||
margin-right: auto;
|
||||
}
|
||||
|
||||
canvas {
|
||||
display: block;
|
||||
width: 100%;
|
||||
height: auto;
|
||||
background: #ffffff;
|
||||
}
|
||||
|
||||
.overlay {
|
||||
position: absolute;
|
||||
border: 1px dashed var(--accent);
|
||||
border-radius: 10px;
|
||||
padding: 8px;
|
||||
background: rgba(255, 255, 255, 0.96);
|
||||
box-shadow: 0 12px 30px rgba(165, 36, 49, 0.15);
|
||||
user-select: none;
|
||||
touch-action: none;
|
||||
}
|
||||
|
||||
.overlay.signature img {
|
||||
display: block;
|
||||
max-width: 100%;
|
||||
max-height: 100%;
|
||||
}
|
||||
|
||||
.overlay-controls {
|
||||
position: absolute;
|
||||
top: -44px;
|
||||
right: 0;
|
||||
display: flex;
|
||||
gap: 6px;
|
||||
}
|
||||
|
||||
.overlay-btn {
|
||||
background: #ffffff;
|
||||
color: var(--accent-strong);
|
||||
border: 1px solid var(--accent);
|
||||
border-radius: 8px;
|
||||
padding: 6px 10px;
|
||||
cursor: pointer;
|
||||
box-shadow: 0 6px 16px rgba(165, 36, 49, 0.16);
|
||||
}
|
||||
|
||||
.overlay-input {
|
||||
border: 1px solid var(--border);
|
||||
background: #ffffff;
|
||||
color: var(--text);
|
||||
font-size: 18px;
|
||||
padding: 6px 8px;
|
||||
min-width: 180px;
|
||||
border-radius: 8px;
|
||||
outline: none;
|
||||
}
|
||||
|
||||
.paging {
|
||||
margin-top: 14px;
|
||||
display: flex;
|
||||
gap: 10px;
|
||||
align-items: center;
|
||||
color: var(--muted);
|
||||
}
|
||||
|
||||
.modal-backdrop {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
background: rgba(15, 23, 42, 0.35);
|
||||
backdrop-filter: blur(3px);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
z-index: 2000;
|
||||
}
|
||||
|
||||
.modal {
|
||||
background: var(--card);
|
||||
border: 1px solid var(--border);
|
||||
border-radius: 14px;
|
||||
padding: 18px;
|
||||
min-width: 760px;
|
||||
box-shadow: var(--shadow);
|
||||
}
|
||||
|
||||
.modal-row {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
margin-top: 12px;
|
||||
color: var(--muted);
|
||||
}
|
||||
|
||||
.modal canvas {
|
||||
background: #ffffff;
|
||||
border: 1px solid var(--border);
|
||||
border-radius: 10px;
|
||||
}
|
||||
|
||||
.spacer {
|
||||
flex: 1;
|
||||
}
|
||||
19
EnvelopeGenerator.ReceiverUIBlazor/wwwroot/index.html
Normal file
19
EnvelopeGenerator.ReceiverUIBlazor/wwwroot/index.html
Normal file
@@ -0,0 +1,19 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>Receiver UI (Blazor)</title>
|
||||
<base href="/" />
|
||||
<link rel="stylesheet" href="css/app.css" />
|
||||
<!-- pdf.js 3.11 UMD + classic worker for compatibility; SRI removed to avoid digest mismatches -->
|
||||
<script src="https://cdnjs.cloudflare.com/ajax/libs/pdf.js/3.11.174/pdf.min.js"></script>
|
||||
<script src="https://cdnjs.cloudflare.com/ajax/libs/pdf.js/3.11.174/pdf.worker.min.js"></script>
|
||||
<script src="https://unpkg.com/pdf-lib/dist/pdf-lib.min.js"></script>
|
||||
<script src="js/pdfInterop.js"></script>
|
||||
</head>
|
||||
<body>
|
||||
<div id="app">Loading...</div>
|
||||
<script src="_framework/blazor.webassembly.js"></script>
|
||||
</body>
|
||||
</html>
|
||||
339
EnvelopeGenerator.ReceiverUIBlazor/wwwroot/js/pdfInterop.js
Normal file
339
EnvelopeGenerator.ReceiverUIBlazor/wwwroot/js/pdfInterop.js
Normal file
@@ -0,0 +1,339 @@
|
||||
(function () {
|
||||
// Stick to pdf.js 3.11 UMD + classic worker for compatibility.
|
||||
const PDF_JS_SRC = "https://cdnjs.cloudflare.com/ajax/libs/pdf.js/3.11.174/pdf.min.js";
|
||||
const WORKER_SRC = "https://cdnjs.cloudflare.com/ajax/libs/pdf.js/3.11.174/pdf.worker.min.js";
|
||||
|
||||
const state = {
|
||||
pdfDoc: null,
|
||||
pdfBytes: null,
|
||||
lastViewport: null,
|
||||
pdfJsReady: null,
|
||||
};
|
||||
|
||||
function base64ToUint8(base64) {
|
||||
const binStr = atob(base64);
|
||||
const len = binStr.length;
|
||||
const bytes = new Uint8Array(len);
|
||||
for (let i = 0; i < len; i++) {
|
||||
bytes[i] = binStr.charCodeAt(i);
|
||||
}
|
||||
return bytes;
|
||||
}
|
||||
|
||||
async function reloadFromBase64(base64) {
|
||||
state.pdfBytes = base64ToUint8(base64);
|
||||
state.pdfDoc = await pdfjsLib.getDocument({ data: state.pdfBytes }).promise;
|
||||
return { pages: state.pdfDoc.numPages };
|
||||
}
|
||||
|
||||
function dataUrlDownload(dataUrl, filename) {
|
||||
const a = document.createElement('a');
|
||||
a.href = dataUrl;
|
||||
a.download = filename;
|
||||
document.body.appendChild(a);
|
||||
a.click();
|
||||
document.body.removeChild(a);
|
||||
}
|
||||
|
||||
const pointerPads = new Map();
|
||||
|
||||
function loadScriptOnce(url) {
|
||||
return new Promise((resolve, reject) => {
|
||||
// If already present, resolve immediately
|
||||
const existing = Array.from(document.getElementsByTagName('script')).find(s => s.src === url);
|
||||
if (existing && existing.dataset.loaded === "true") {
|
||||
resolve();
|
||||
return;
|
||||
}
|
||||
|
||||
const script = existing || document.createElement('script');
|
||||
script.src = url;
|
||||
script.defer = true;
|
||||
script.onload = () => {
|
||||
script.dataset.loaded = "true";
|
||||
resolve();
|
||||
};
|
||||
script.onerror = (e) => reject(new Error(`Script load failed: ${url}`));
|
||||
|
||||
if (!existing) {
|
||||
document.head.appendChild(script);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
async function ensurePdfJsLoaded() {
|
||||
if (typeof pdfjsLib !== "undefined") {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!state.pdfJsReady) {
|
||||
state.pdfJsReady = loadScriptOnce(PDF_JS_SRC);
|
||||
}
|
||||
|
||||
await state.pdfJsReady;
|
||||
|
||||
if (typeof pdfjsLib === "undefined") {
|
||||
throw new Error("pdfjsLib could not be loaded");
|
||||
}
|
||||
}
|
||||
|
||||
window.pdfInterop = {
|
||||
ensureReady: async () => {
|
||||
// Ensure pdf.js is present and the worker path is set explicitly.
|
||||
await ensurePdfJsLoaded();
|
||||
if (pdfjsLib && pdfjsLib.GlobalWorkerOptions) {
|
||||
if (pdfjsLib.GlobalWorkerOptions.workerSrc !== WORKER_SRC) {
|
||||
pdfjsLib.GlobalWorkerOptions.workerSrc = WORKER_SRC;
|
||||
}
|
||||
} else {
|
||||
throw new Error("pdf.js not available after load");
|
||||
}
|
||||
},
|
||||
loadPdf: async (base64) => {
|
||||
await ensurePdfJsLoaded();
|
||||
try {
|
||||
const result = await reloadFromBase64(base64);
|
||||
if (!result || !result.pages) {
|
||||
throw new Error("PDF has keine Seiten erkannt");
|
||||
}
|
||||
return result;
|
||||
} catch (err) {
|
||||
console.error("pdfInterop.loadPdf failed", err);
|
||||
throw err;
|
||||
}
|
||||
},
|
||||
renderPage: async (pageIndex, canvasId, targetWidth) => {
|
||||
await ensurePdfJsLoaded();
|
||||
if (!state.pdfDoc) {
|
||||
throw new Error('PDF not loaded');
|
||||
}
|
||||
const page = await state.pdfDoc.getPage(pageIndex + 1);
|
||||
const rawViewport = page.getViewport({ scale: 1 });
|
||||
const scale = targetWidth / rawViewport.width;
|
||||
const viewport = page.getViewport({ scale });
|
||||
|
||||
let canvas = document.getElementById(canvasId);
|
||||
if (!canvas) {
|
||||
// give the UI a tiny delay to render the canvas into the DOM
|
||||
await new Promise(r => setTimeout(r, 40));
|
||||
canvas = document.getElementById(canvasId);
|
||||
}
|
||||
if (!canvas) {
|
||||
console.error("renderPage: canvas not found", canvasId);
|
||||
throw new Error('Canvas not found');
|
||||
}
|
||||
const ctx = canvas.getContext('2d');
|
||||
canvas.width = viewport.width;
|
||||
canvas.height = viewport.height;
|
||||
|
||||
await page.render({ canvasContext: ctx, viewport }).promise;
|
||||
|
||||
state.lastViewport = {
|
||||
width: viewport.width,
|
||||
height: viewport.height,
|
||||
pageWidth: rawViewport.width,
|
||||
pageHeight: rawViewport.height,
|
||||
};
|
||||
|
||||
return state.lastViewport;
|
||||
},
|
||||
applySignature: async (payload) => {
|
||||
const {
|
||||
base64,
|
||||
pageIndex,
|
||||
left,
|
||||
top,
|
||||
width,
|
||||
height,
|
||||
renderWidth,
|
||||
renderHeight,
|
||||
dataUrl,
|
||||
autoDate,
|
||||
} = payload;
|
||||
|
||||
const pdfDoc = await PDFLib.PDFDocument.load(base64ToUint8(base64));
|
||||
const page = pdfDoc.getPage(pageIndex);
|
||||
const scaleX = page.getWidth() / renderWidth;
|
||||
const scaleY = page.getHeight() / renderHeight;
|
||||
|
||||
const pngImage = await pdfDoc.embedPng(dataUrl);
|
||||
const drawWidth = width * scaleX;
|
||||
const drawHeight = height * scaleY;
|
||||
const x = left * scaleX;
|
||||
const y = page.getHeight() - (top + height) * scaleY;
|
||||
|
||||
page.drawImage(pngImage, {
|
||||
x,
|
||||
y,
|
||||
width: drawWidth,
|
||||
height: drawHeight,
|
||||
});
|
||||
|
||||
if (autoDate) {
|
||||
const text = `Signed ${new Date().toLocaleString()}`;
|
||||
page.drawText(text, {
|
||||
x,
|
||||
y: y - 14 * scaleY,
|
||||
size: 14 * scaleX,
|
||||
color: PDFLib.rgb(0.11, 0.25, 0.56),
|
||||
});
|
||||
}
|
||||
|
||||
const updatedBase64 = await pdfDoc.saveAsBase64({ dataUri: false });
|
||||
await reloadFromBase64(updatedBase64);
|
||||
return updatedBase64;
|
||||
},
|
||||
applyText: async (payload) => {
|
||||
const {
|
||||
base64,
|
||||
pageIndex,
|
||||
left,
|
||||
top,
|
||||
width,
|
||||
height,
|
||||
renderWidth,
|
||||
renderHeight,
|
||||
text,
|
||||
fontSize,
|
||||
} = payload;
|
||||
|
||||
const pdfDoc = await PDFLib.PDFDocument.load(base64ToUint8(base64));
|
||||
const page = pdfDoc.getPage(pageIndex);
|
||||
const scaleX = page.getWidth() / renderWidth;
|
||||
const scaleY = page.getHeight() / renderHeight;
|
||||
|
||||
const x = left * scaleX;
|
||||
const y = page.getHeight() - (top + height) * scaleY;
|
||||
|
||||
page.drawText(text, {
|
||||
x,
|
||||
y,
|
||||
size: fontSize * scaleX,
|
||||
color: PDFLib.rgb(0.2, 0.23, 0.28),
|
||||
});
|
||||
|
||||
const updatedBase64 = await pdfDoc.saveAsBase64({ dataUri: false });
|
||||
await reloadFromBase64(updatedBase64);
|
||||
return updatedBase64;
|
||||
},
|
||||
downloadPdf: (base64, filename) => {
|
||||
dataUrlDownload(`data:application/pdf;base64,${base64}`, filename);
|
||||
},
|
||||
initSignaturePad: (canvasId) => {
|
||||
const canvas = document.getElementById(canvasId);
|
||||
if (!canvas) return;
|
||||
const ctx = canvas.getContext('2d');
|
||||
ctx.lineWidth = 2;
|
||||
ctx.lineCap = 'round';
|
||||
ctx.strokeStyle = '#1c3d8f';
|
||||
|
||||
const padState = {
|
||||
drawing: false,
|
||||
lastX: 0,
|
||||
lastY: 0,
|
||||
};
|
||||
|
||||
function getPos(evt) {
|
||||
const rect = canvas.getBoundingClientRect();
|
||||
const scaleX = rect.width ? canvas.width / rect.width : 1;
|
||||
const scaleY = rect.height ? canvas.height / rect.height : 1;
|
||||
return {
|
||||
x: (evt.clientX - rect.left) * scaleX,
|
||||
y: (evt.clientY - rect.top) * scaleY,
|
||||
};
|
||||
}
|
||||
|
||||
function start(e) {
|
||||
padState.drawing = true;
|
||||
const pos = getPos(e);
|
||||
padState.lastX = pos.x;
|
||||
padState.lastY = pos.y;
|
||||
}
|
||||
|
||||
function move(e) {
|
||||
if (!padState.drawing) return;
|
||||
const pos = getPos(e);
|
||||
const x = pos.x;
|
||||
const y = pos.y;
|
||||
ctx.beginPath();
|
||||
ctx.moveTo(padState.lastX, padState.lastY);
|
||||
ctx.lineTo(x, y);
|
||||
ctx.stroke();
|
||||
padState.lastX = x;
|
||||
padState.lastY = y;
|
||||
}
|
||||
|
||||
function end() {
|
||||
padState.drawing = false;
|
||||
}
|
||||
|
||||
canvas.onpointerdown = start;
|
||||
canvas.onpointermove = move;
|
||||
canvas.onpointerup = end;
|
||||
canvas.onpointerleave = end;
|
||||
|
||||
pointerPads.set(canvasId, { ctx, canvas });
|
||||
},
|
||||
registerDropHandler: (dotNetRef) => {
|
||||
if (window.__pdfDropRegistered) return;
|
||||
window.__pdfDropRegistered = true;
|
||||
|
||||
const prevent = (e) => {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
};
|
||||
|
||||
['dragenter', 'dragover', 'dragleave'].forEach(evt => {
|
||||
document.addEventListener(evt, prevent, false);
|
||||
});
|
||||
|
||||
document.addEventListener('drop', (e) => {
|
||||
prevent(e);
|
||||
|
||||
const files = e.dataTransfer?.files;
|
||||
if (!files || files.length === 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
const file = files[0];
|
||||
const reader = new FileReader();
|
||||
reader.onload = () => {
|
||||
const result = reader.result;
|
||||
if (typeof result === 'string') {
|
||||
const base64 = result.split(',')[1] || result;
|
||||
dotNetRef?.invokeMethodAsync('LoadPdfFromBase64', base64);
|
||||
}
|
||||
};
|
||||
reader.readAsDataURL(file);
|
||||
}, false);
|
||||
},
|
||||
clearSignaturePad: (canvasId) => {
|
||||
const pad = pointerPads.get(canvasId);
|
||||
if (!pad) return;
|
||||
pad.ctx.clearRect(0, 0, pad.canvas.width, pad.canvas.height);
|
||||
},
|
||||
getSignatureDataUrl: (canvasId) => {
|
||||
const pad = pointerPads.get(canvasId);
|
||||
if (!pad) return null;
|
||||
return pad.canvas.toDataURL('image/png');
|
||||
},
|
||||
capturePointer: (element, pointerId) => {
|
||||
if (element && element.setPointerCapture) {
|
||||
try {
|
||||
element.setPointerCapture(pointerId);
|
||||
} catch (err) {
|
||||
console.warn('capturePointer failed', err);
|
||||
}
|
||||
}
|
||||
},
|
||||
releasePointer: (element, pointerId) => {
|
||||
if (element && element.releasePointerCapture) {
|
||||
try {
|
||||
element.releasePointerCapture(pointerId);
|
||||
} catch (err) {
|
||||
console.warn('releasePointer failed', err);
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
})();
|
||||
@@ -59,7 +59,7 @@
|
||||
</dependentAssembly>
|
||||
<dependentAssembly>
|
||||
<assemblyIdentity name="Microsoft.Extensions.Logging.Abstractions" publicKeyToken="adb9793829ddae60" culture="neutral" />
|
||||
<bindingRedirect oldVersion="0.0.0.0-2.1.1.0" newVersion="2.1.1.0" />
|
||||
<bindingRedirect oldVersion="0.0.0.0-7.0.0.0" newVersion="7.0.0.0" />
|
||||
</dependentAssembly>
|
||||
<dependentAssembly>
|
||||
<assemblyIdentity name="System.Text.Encodings.Web" publicKeyToken="cc7b13ffcd2ddd51" culture="neutral" />
|
||||
|
||||
@@ -17,6 +17,21 @@
|
||||
<TargetFrameworkProfile />
|
||||
<NuGetPackageImportStamp>
|
||||
</NuGetPackageImportStamp>
|
||||
<PublishUrl>publish\</PublishUrl>
|
||||
<Install>true</Install>
|
||||
<InstallFrom>Disk</InstallFrom>
|
||||
<UpdateEnabled>false</UpdateEnabled>
|
||||
<UpdateMode>Foreground</UpdateMode>
|
||||
<UpdateInterval>7</UpdateInterval>
|
||||
<UpdateIntervalUnits>Days</UpdateIntervalUnits>
|
||||
<UpdatePeriodically>false</UpdatePeriodically>
|
||||
<UpdateRequired>false</UpdateRequired>
|
||||
<MapFileExtensions>true</MapFileExtensions>
|
||||
<ApplicationRevision>0</ApplicationRevision>
|
||||
<ApplicationVersion>1.0.0.%2a</ApplicationVersion>
|
||||
<IsWebBootstrapper>false</IsWebBootstrapper>
|
||||
<UseApplicationTrust>false</UseApplicationTrust>
|
||||
<BootstrapperEnabled>true</BootstrapperEnabled>
|
||||
</PropertyGroup>
|
||||
<PropertyGroup Condition=" '$(Configuration)|$(Platform)' == 'Debug|AnyCPU' ">
|
||||
<PlatformTarget>AnyCPU</PlatformTarget>
|
||||
@@ -54,6 +69,9 @@
|
||||
<Reference Include="BouncyCastle.Cryptography, Version=2.0.0.0, Culture=neutral, PublicKeyToken=072edcf4a5328938, processorArchitecture=MSIL">
|
||||
<HintPath>..\packages\BouncyCastle.Cryptography.2.5.0\lib\net461\BouncyCastle.Cryptography.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>
|
||||
</Reference>
|
||||
<Reference Include="DigitalData.Modules.Base, Version=1.3.8.0, Culture=neutral, processorArchitecture=MSIL">
|
||||
<SpecificVersion>False</SpecificVersion>
|
||||
<HintPath>..\..\2_DLL Projekte\DDModules\Base\bin\Debug\DigitalData.Modules.Base.dll</HintPath>
|
||||
@@ -160,8 +178,14 @@
|
||||
<HintPath>..\packages\Microsoft.Bcl.AsyncInterfaces.8.0.0\lib\net462\Microsoft.Bcl.AsyncInterfaces.dll</HintPath>
|
||||
</Reference>
|
||||
<Reference Include="Microsoft.CSharp" />
|
||||
<Reference Include="Microsoft.Extensions.Logging.Abstractions, Version=2.1.1.0, Culture=neutral, PublicKeyToken=adb9793829ddae60, processorArchitecture=MSIL">
|
||||
<HintPath>..\packages\Microsoft.Extensions.Logging.Abstractions.2.1.1\lib\netstandard2.0\Microsoft.Extensions.Logging.Abstractions.dll</HintPath>
|
||||
<Reference Include="Microsoft.Extensions.DependencyInjection, Version=7.0.0.0, Culture=neutral, PublicKeyToken=adb9793829ddae60, processorArchitecture=MSIL">
|
||||
<HintPath>..\packages\Microsoft.Extensions.DependencyInjection.7.0.0\lib\net462\Microsoft.Extensions.DependencyInjection.dll</HintPath>
|
||||
</Reference>
|
||||
<Reference Include="Microsoft.Extensions.DependencyInjection.Abstractions, Version=7.0.0.0, Culture=neutral, PublicKeyToken=adb9793829ddae60, processorArchitecture=MSIL">
|
||||
<HintPath>..\packages\Microsoft.Extensions.DependencyInjection.Abstractions.7.0.0\lib\net462\Microsoft.Extensions.DependencyInjection.Abstractions.dll</HintPath>
|
||||
</Reference>
|
||||
<Reference Include="Microsoft.Extensions.Logging.Abstractions, Version=7.0.0.0, Culture=neutral, PublicKeyToken=adb9793829ddae60, processorArchitecture=MSIL">
|
||||
<HintPath>..\packages\Microsoft.Extensions.Logging.Abstractions.7.0.0\lib\net462\Microsoft.Extensions.Logging.Abstractions.dll</HintPath>
|
||||
</Reference>
|
||||
<Reference Include="Newtonsoft.Json, Version=13.0.0.0, Culture=neutral, PublicKeyToken=30ad4fe6b2a6aeed, processorArchitecture=MSIL">
|
||||
<HintPath>..\packages\Newtonsoft.Json.13.0.3\lib\net45\Newtonsoft.Json.dll</HintPath>
|
||||
@@ -335,6 +359,18 @@
|
||||
<Name>EnvelopeGenerator.Domain</Name>
|
||||
</ProjectReference>
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<BootstrapperPackage Include=".NETFramework,Version=v4.6.2">
|
||||
<Visible>False</Visible>
|
||||
<ProductName>Microsoft .NET Framework 4.6.2 %28x86 and x64%29</ProductName>
|
||||
<Install>true</Install>
|
||||
</BootstrapperPackage>
|
||||
<BootstrapperPackage Include="Microsoft.Net.Framework.3.5.SP1">
|
||||
<Visible>False</Visible>
|
||||
<ProductName>.NET Framework 3.5 SP1</ProductName>
|
||||
<Install>false</Install>
|
||||
</BootstrapperPackage>
|
||||
</ItemGroup>
|
||||
<Import Project="$(MSBuildToolsPath)\Microsoft.VisualBasic.targets" />
|
||||
<Import Project="..\packages\GdPicture.runtimes.windows.14.3.3\build\net462\GdPicture.runtimes.windows.targets" Condition="Exists('..\packages\GdPicture.runtimes.windows.14.3.3\build\net462\GdPicture.runtimes.windows.targets')" />
|
||||
<Target Name="EnsureNuGetPackageBuildImports" BeforeTargets="PrepareForBuild">
|
||||
|
||||
16
EnvelopeGenerator.Service/UserConfig.xml
Normal file
16
EnvelopeGenerator.Service/UserConfig.xml
Normal file
@@ -0,0 +1,16 @@
|
||||
<?xml version="1.0"?>
|
||||
<Config xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:xsd="http://www.w3.org/2001/XMLSchema">
|
||||
<ConnectionString>Server=sDD-VMP04-SQL17\DD_DEVELOP01;Database=DD_ECM;User Id=sa;Password=dd;TrustServerCertificate=True;</ConnectionString>
|
||||
<Debug>true</Debug>
|
||||
<IntervalInMin>1</IntervalInMin>
|
||||
<IgnoredLabels>
|
||||
<Label>Date</Label>
|
||||
<Label>Datum</Label>
|
||||
<Label>ZIP</Label>
|
||||
<Label>PLZ</Label>
|
||||
<Label>Place</Label>
|
||||
<Label>Ort</Label>
|
||||
<Label>Position</Label>
|
||||
<Label>Stellung</Label>
|
||||
</IgnoredLabels>
|
||||
</Config>
|
||||
@@ -1,6 +1,7 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<packages>
|
||||
<package id="BouncyCastle.Cryptography" version="2.5.0" targetFramework="net48" />
|
||||
<package id="DigitalData.Core.Abstractions" version="4.3.0" targetFramework="net462" />
|
||||
<package id="DocumentFormat.OpenXml" version="3.2.0" targetFramework="net48" />
|
||||
<package id="DocumentFormat.OpenXml.Framework" version="3.2.0" targetFramework="net48" />
|
||||
<package id="EntityFramework" version="6.4.4" targetFramework="net48" />
|
||||
@@ -11,7 +12,9 @@
|
||||
<package id="Microsoft.AspNet.WebApi.Client" version="6.0.0" targetFramework="net48" />
|
||||
<package id="Microsoft.Bcl.AsyncInterfaces" version="8.0.0" targetFramework="net48" />
|
||||
<package id="Microsoft.CSharp" version="4.7.0" targetFramework="net48" />
|
||||
<package id="Microsoft.Extensions.Logging.Abstractions" version="2.1.1" targetFramework="net462" />
|
||||
<package id="Microsoft.Extensions.DependencyInjection" version="7.0.0" targetFramework="net462" />
|
||||
<package id="Microsoft.Extensions.DependencyInjection.Abstractions" version="7.0.0" targetFramework="net462" />
|
||||
<package id="Microsoft.Extensions.Logging.Abstractions" version="7.0.0" targetFramework="net462" />
|
||||
<package id="Microsoft.VisualBasic" version="10.3.0" targetFramework="net48" />
|
||||
<package id="Newtonsoft.Json" version="13.0.3" targetFramework="net48" />
|
||||
<package id="Newtonsoft.Json.Bson" version="1.0.2" targetFramework="net48" />
|
||||
|
||||
@@ -21,7 +21,7 @@
|
||||
<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.Abstractions" Version="4.2.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" />
|
||||
<PackageReference Include="Microsoft.EntityFrameworkCore" Version="7.0.20" />
|
||||
|
||||
52
EnvelopeGenerator.Web/Client/receiver-ui-react/package.json
Normal file
52
EnvelopeGenerator.Web/Client/receiver-ui-react/package.json
Normal file
@@ -0,0 +1,52 @@
|
||||
{
|
||||
"homepage": "https://slavik0329.github.io/pdf-sign",
|
||||
"name": "pdf-sign",
|
||||
"version": "0.1.0",
|
||||
"private": true,
|
||||
"dependencies": {
|
||||
"@testing-library/jest-dom": "^5.11.4",
|
||||
"@testing-library/react": "^11.1.0",
|
||||
"@testing-library/user-event": "^12.1.10",
|
||||
"annotpdf": "^1.0.12",
|
||||
"dayjs": "^1.9.6",
|
||||
"pdf-lib": "^1.12.0",
|
||||
"react": "^17.0.1",
|
||||
"react-dom": "^17.0.1",
|
||||
"react-draggable": "^4.4.3",
|
||||
"react-dropzone": "^11.2.4",
|
||||
"react-icons": "^4.1.0",
|
||||
"react-pdf": "^5.0.0",
|
||||
"react-scripts": "4.0.1",
|
||||
"react-signature-canvas": "^1.0.3",
|
||||
"web-vitals": "^0.2.4"
|
||||
},
|
||||
"scripts": {
|
||||
"start": "react-scripts start",
|
||||
"build": "react-scripts build",
|
||||
"test": "react-scripts test",
|
||||
"eject": "react-scripts eject",
|
||||
"predeploy": "npm run build",
|
||||
"deploy": "gh-pages -d build"
|
||||
},
|
||||
"eslintConfig": {
|
||||
"extends": [
|
||||
"react-app",
|
||||
"react-app/jest"
|
||||
]
|
||||
},
|
||||
"browserslist": {
|
||||
"production": [
|
||||
">0.2%",
|
||||
"not dead",
|
||||
"not op_mini all"
|
||||
],
|
||||
"development": [
|
||||
"last 1 chrome version",
|
||||
"last 1 firefox version",
|
||||
"last 1 safari version"
|
||||
]
|
||||
},
|
||||
"devDependencies": {
|
||||
"gh-pages": "^3.1.0"
|
||||
}
|
||||
}
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 3.8 KiB |
@@ -0,0 +1,44 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<link rel="icon" href="%PUBLIC_URL%/favicon.ico" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||
<meta name="theme-color" content="#000000" />
|
||||
<meta
|
||||
name="description"
|
||||
content="Finally, a simple way to put your signature on a PDF for free."
|
||||
/>
|
||||
<link rel="apple-touch-icon" href="%PUBLIC_URL%/logo192.png" />
|
||||
<!--
|
||||
manifest.json provides metadata used when your web app is installed on a
|
||||
user's mobile device or desktop. See https://developers.google.com/web/fundamentals/web-app-manifest/
|
||||
-->
|
||||
<link rel="manifest" href="%PUBLIC_URL%/manifest.json" />
|
||||
<!--
|
||||
Notice the use of %PUBLIC_URL% in the tags above.
|
||||
It will be replaced with the URL of the `public` folder during the build.
|
||||
Only files inside the `public` folder can be referenced from the HTML.
|
||||
|
||||
Unlike "/favicon.ico" or "favicon.ico", "%PUBLIC_URL%/favicon.ico" will
|
||||
work correctly both with client-side routing and a non-root public URL.
|
||||
Learn how to configure a non-root public URL by running `npm run build`.
|
||||
-->
|
||||
<title>PDF-sign</title>
|
||||
<link href="https://fonts.googleapis.com/css2?family=Open+Sans:wght@300;400;600;800&display=swap" rel="stylesheet">
|
||||
</head>
|
||||
<body>
|
||||
<noscript>You need to enable JavaScript to run this app.</noscript>
|
||||
<div id="root"></div>
|
||||
<!--
|
||||
This HTML file is a template.
|
||||
If you open it directly in the browser, you will see an empty page.
|
||||
|
||||
You can add webfonts, meta tags, or analytics to this file.
|
||||
The build step will place the bundled scripts into the <body> tag.
|
||||
|
||||
To begin the development, run `npm start` or `yarn start`.
|
||||
To create a production bundle, use `npm run build` or `yarn build`.
|
||||
-->
|
||||
</body>
|
||||
</html>
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 5.2 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 9.4 KiB |
@@ -0,0 +1,25 @@
|
||||
{
|
||||
"short_name": "React App",
|
||||
"name": "Create React App Sample",
|
||||
"icons": [
|
||||
{
|
||||
"src": "favicon.ico",
|
||||
"sizes": "64x64 32x32 24x24 16x16",
|
||||
"type": "image/x-icon"
|
||||
},
|
||||
{
|
||||
"src": "logo192.png",
|
||||
"type": "image/png",
|
||||
"sizes": "192x192"
|
||||
},
|
||||
{
|
||||
"src": "logo512.png",
|
||||
"type": "image/png",
|
||||
"sizes": "512x512"
|
||||
}
|
||||
],
|
||||
"start_url": ".",
|
||||
"display": "standalone",
|
||||
"theme_color": "#000000",
|
||||
"background_color": "#ffffff"
|
||||
}
|
||||
10
EnvelopeGenerator.Web/Client/receiver-ui-react/src/App.css
Normal file
10
EnvelopeGenerator.Web/Client/receiver-ui-react/src/App.css
Normal file
@@ -0,0 +1,10 @@
|
||||
body {
|
||||
font-family: "Open Sans";
|
||||
}
|
||||
|
||||
input:focus,
|
||||
select:focus,
|
||||
textarea:focus,
|
||||
button:focus {
|
||||
outline: none;
|
||||
}
|
||||
278
EnvelopeGenerator.Web/Client/receiver-ui-react/src/App.js
Normal file
278
EnvelopeGenerator.Web/Client/receiver-ui-react/src/App.js
Normal file
@@ -0,0 +1,278 @@
|
||||
import "./App.css";
|
||||
import { useRef, useState } from "react";
|
||||
import Drop from "./Drop";
|
||||
import { Document, Page, pdfjs } from "react-pdf";
|
||||
import { PDFDocument, rgb } from "pdf-lib";
|
||||
import { blobToURL } from "./utils/Utils";
|
||||
import PagingControl from "./components/PagingControl";
|
||||
import { AddSigDialog } from "./components/AddSigDialog";
|
||||
import { Header } from "./Header";
|
||||
import { BigButton } from "./components/BigButton";
|
||||
import DraggableSignature from "./components/DraggableSignature";
|
||||
import DraggableText from "./components/DraggableText";
|
||||
import dayjs from "dayjs";
|
||||
|
||||
pdfjs.GlobalWorkerOptions.workerSrc = `//cdnjs.cloudflare.com/ajax/libs/pdf.js/${pdfjs.version}/pdf.worker.js`;
|
||||
|
||||
function downloadURI(uri, name) {
|
||||
var link = document.createElement("a");
|
||||
link.download = name;
|
||||
link.href = uri;
|
||||
document.body.appendChild(link);
|
||||
link.click();
|
||||
document.body.removeChild(link);
|
||||
}
|
||||
|
||||
function App() {
|
||||
const styles = {
|
||||
container: {
|
||||
maxWidth: 900,
|
||||
margin: "0 auto",
|
||||
},
|
||||
sigBlock: {
|
||||
display: "inline-block",
|
||||
border: "1px solid #000",
|
||||
},
|
||||
documentBlock: {
|
||||
maxWidth: 800,
|
||||
margin: "20px auto",
|
||||
marginTop: 8,
|
||||
border: "1px solid #999",
|
||||
},
|
||||
controls: {
|
||||
maxWidth: 800,
|
||||
margin: "0 auto",
|
||||
marginTop: 8,
|
||||
},
|
||||
};
|
||||
const [pdf, setPdf] = useState(null);
|
||||
const [autoDate, setAutoDate] = useState(true);
|
||||
const [signatureURL, setSignatureURL] = useState(null);
|
||||
const [position, setPosition] = useState(null);
|
||||
const [signatureDialogVisible, setSignatureDialogVisible] = useState(false);
|
||||
const [textInputVisible, setTextInputVisible] = useState(false);
|
||||
const [pageNum, setPageNum] = useState(0);
|
||||
const [totalPages, setTotalPages] = useState(0);
|
||||
const [pageDetails, setPageDetails] = useState(null);
|
||||
const documentRef = useRef(null);
|
||||
|
||||
return (
|
||||
<div>
|
||||
<Header />
|
||||
<div style={styles.container}>
|
||||
{signatureDialogVisible ? (
|
||||
<AddSigDialog
|
||||
autoDate={autoDate}
|
||||
setAutoDate={setAutoDate}
|
||||
onClose={() => setSignatureDialogVisible(false)}
|
||||
onConfirm={(url) => {
|
||||
setSignatureURL(url);
|
||||
setSignatureDialogVisible(false);
|
||||
}}
|
||||
/>
|
||||
) : null}
|
||||
|
||||
{!pdf ? (
|
||||
<Drop
|
||||
onLoaded={async (files) => {
|
||||
const URL = await blobToURL(files[0]);
|
||||
setPdf(URL);
|
||||
}}
|
||||
/>
|
||||
) : null}
|
||||
{pdf ? (
|
||||
<div>
|
||||
<div style={styles.controls}>
|
||||
{!signatureURL ? (
|
||||
<BigButton
|
||||
marginRight={8}
|
||||
title={"Add signature"}
|
||||
onClick={() => setSignatureDialogVisible(true)}
|
||||
/>
|
||||
) : null}
|
||||
|
||||
<BigButton
|
||||
marginRight={8}
|
||||
title={"Add Date"}
|
||||
onClick={() => setTextInputVisible("date")}
|
||||
/>
|
||||
|
||||
<BigButton
|
||||
marginRight={8}
|
||||
title={"Add Text"}
|
||||
onClick={() => setTextInputVisible(true)}
|
||||
/>
|
||||
<BigButton
|
||||
marginRight={8}
|
||||
title={"Reset"}
|
||||
onClick={() => {
|
||||
setTextInputVisible(false);
|
||||
setSignatureDialogVisible(false);
|
||||
setSignatureURL(null);
|
||||
setPdf(null);
|
||||
setTotalPages(0);
|
||||
setPageNum(0);
|
||||
setPageDetails(null);
|
||||
}}
|
||||
/>
|
||||
{pdf ? (
|
||||
<BigButton
|
||||
marginRight={8}
|
||||
inverted={true}
|
||||
title={"Download"}
|
||||
onClick={() => {
|
||||
downloadURI(pdf, "file.pdf");
|
||||
}}
|
||||
/>
|
||||
) : null}
|
||||
</div>
|
||||
<div ref={documentRef} style={styles.documentBlock}>
|
||||
{textInputVisible ? (
|
||||
<DraggableText
|
||||
initialText={
|
||||
textInputVisible === "date"
|
||||
? dayjs().format("M/d/YYYY")
|
||||
: null
|
||||
}
|
||||
onCancel={() => setTextInputVisible(false)}
|
||||
onEnd={setPosition}
|
||||
onSet={async (text) => {
|
||||
const { originalHeight, originalWidth } = pageDetails;
|
||||
const scale = originalWidth / documentRef.current.clientWidth;
|
||||
|
||||
const y =
|
||||
documentRef.current.clientHeight -
|
||||
(position.y +
|
||||
(12 * scale) -
|
||||
position.offsetY -
|
||||
documentRef.current.offsetTop);
|
||||
const x =
|
||||
position.x -
|
||||
166 -
|
||||
position.offsetX -
|
||||
documentRef.current.offsetLeft;
|
||||
|
||||
// new XY in relation to actual document size
|
||||
const newY =
|
||||
(y * originalHeight) / documentRef.current.clientHeight;
|
||||
const newX =
|
||||
(x * originalWidth) / documentRef.current.clientWidth;
|
||||
|
||||
const pdfDoc = await PDFDocument.load(pdf);
|
||||
|
||||
const pages = pdfDoc.getPages();
|
||||
const firstPage = pages[pageNum];
|
||||
|
||||
firstPage.drawText(text, {
|
||||
x: newX,
|
||||
y: newY,
|
||||
size: 20 * scale,
|
||||
});
|
||||
|
||||
const pdfBytes = await pdfDoc.save();
|
||||
const blob = new Blob([new Uint8Array(pdfBytes)]);
|
||||
|
||||
const URL = await blobToURL(blob);
|
||||
setPdf(URL);
|
||||
setPosition(null);
|
||||
setTextInputVisible(false);
|
||||
}}
|
||||
/>
|
||||
) : null}
|
||||
{signatureURL ? (
|
||||
<DraggableSignature
|
||||
url={signatureURL}
|
||||
onCancel={() => {
|
||||
setSignatureURL(null);
|
||||
}}
|
||||
onSet={async () => {
|
||||
const { originalHeight, originalWidth } = pageDetails;
|
||||
const scale = originalWidth / documentRef.current.clientWidth;
|
||||
|
||||
const y =
|
||||
documentRef.current.clientHeight -
|
||||
(position.y -
|
||||
position.offsetY +
|
||||
64 -
|
||||
documentRef.current.offsetTop);
|
||||
const x =
|
||||
position.x -
|
||||
160 -
|
||||
position.offsetX -
|
||||
documentRef.current.offsetLeft;
|
||||
|
||||
// new XY in relation to actual document size
|
||||
const newY =
|
||||
(y * originalHeight) / documentRef.current.clientHeight;
|
||||
const newX =
|
||||
(x * originalWidth) / documentRef.current.clientWidth;
|
||||
|
||||
const pdfDoc = await PDFDocument.load(pdf);
|
||||
|
||||
const pages = pdfDoc.getPages();
|
||||
const firstPage = pages[pageNum];
|
||||
|
||||
const pngImage = await pdfDoc.embedPng(signatureURL);
|
||||
const pngDims = pngImage.scale( scale * .3);
|
||||
|
||||
firstPage.drawImage(pngImage, {
|
||||
x: newX,
|
||||
y: newY,
|
||||
width: pngDims.width,
|
||||
height: pngDims.height,
|
||||
});
|
||||
|
||||
if (autoDate) {
|
||||
firstPage.drawText(
|
||||
`Signed ${dayjs().format(
|
||||
"M/d/YYYY HH:mm:ss ZZ"
|
||||
)}`,
|
||||
{
|
||||
x: newX,
|
||||
y: newY - 10,
|
||||
size: 14 * scale,
|
||||
color: rgb(0.074, 0.545, 0.262),
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
const pdfBytes = await pdfDoc.save();
|
||||
const blob = new Blob([new Uint8Array(pdfBytes)]);
|
||||
|
||||
const URL = await blobToURL(blob);
|
||||
setPdf(URL);
|
||||
setPosition(null);
|
||||
setSignatureURL(null);
|
||||
}}
|
||||
onEnd={setPosition}
|
||||
/>
|
||||
) : null}
|
||||
<Document
|
||||
file={pdf}
|
||||
onLoadSuccess={(data) => {
|
||||
setTotalPages(data.numPages);
|
||||
}}
|
||||
>
|
||||
<Page
|
||||
pageNumber={pageNum + 1}
|
||||
width={800}
|
||||
height={1200}
|
||||
onLoadSuccess={(data) => {
|
||||
setPageDetails(data);
|
||||
}}
|
||||
/>
|
||||
</Document>
|
||||
</div>
|
||||
<PagingControl
|
||||
pageNum={pageNum}
|
||||
setPageNum={setPageNum}
|
||||
totalPages={totalPages}
|
||||
/>
|
||||
</div>
|
||||
) : null}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default App;
|
||||
@@ -0,0 +1,8 @@
|
||||
import { render, screen } from '@testing-library/react';
|
||||
import App from './App';
|
||||
|
||||
test('renders learn react link', () => {
|
||||
render(<App />);
|
||||
const linkElement = screen.getByText(/learn react/i);
|
||||
expect(linkElement).toBeInTheDocument();
|
||||
});
|
||||
37
EnvelopeGenerator.Web/Client/receiver-ui-react/src/Drop.js
Normal file
37
EnvelopeGenerator.Web/Client/receiver-ui-react/src/Drop.js
Normal file
@@ -0,0 +1,37 @@
|
||||
import React, { useCallback } from "react";
|
||||
import { useDropzone } from "react-dropzone";
|
||||
import { cleanBorder, primary45 } from "./utils/colors";
|
||||
|
||||
export default function Drop({ onLoaded }) {
|
||||
const styles = {
|
||||
container: {
|
||||
textAlign: "center",
|
||||
border: cleanBorder,
|
||||
padding: 20,
|
||||
marginTop: 12,
|
||||
color: primary45,
|
||||
fontSize: 18,
|
||||
fontWeight: 600,
|
||||
borderRadius: 4,
|
||||
userSelect: "none",
|
||||
outline: 0,
|
||||
cursor: "pointer",
|
||||
},
|
||||
};
|
||||
|
||||
const onDrop = useCallback((acceptedFiles) => {
|
||||
onLoaded(acceptedFiles);
|
||||
// Do something with the files
|
||||
}, []);
|
||||
const { getRootProps, getInputProps, isDragActive } = useDropzone({
|
||||
onDrop,
|
||||
accept: "application/pdf",
|
||||
});
|
||||
|
||||
return (
|
||||
<div {...getRootProps()} style={styles.container}>
|
||||
<input {...getInputProps()} />
|
||||
{isDragActive ? <p>Drop a PDF here</p> : <p>Drag a PDF here</p>}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
15
EnvelopeGenerator.Web/Client/receiver-ui-react/src/Header.js
Normal file
15
EnvelopeGenerator.Web/Client/receiver-ui-react/src/Header.js
Normal file
@@ -0,0 +1,15 @@
|
||||
import {primary45} from "./utils/colors";
|
||||
|
||||
export function Header() {
|
||||
const styles = {
|
||||
container: {
|
||||
backgroundColor: primary45,
|
||||
color: '#FFF',
|
||||
padding: 12,
|
||||
fontWeight: 600,
|
||||
}
|
||||
}
|
||||
return <div style={styles.container}>
|
||||
<div>Open PDF Sign</div>
|
||||
</div>
|
||||
}
|
||||
@@ -0,0 +1,77 @@
|
||||
import { Dialog } from "./Dialog";
|
||||
import SignatureCanvas from "react-signature-canvas";
|
||||
import { ConfirmOrCancel } from "./ConfirmOrCancel";
|
||||
import { primary45 } from "../utils/colors";
|
||||
import { useRef } from "react";
|
||||
|
||||
export function AddSigDialog({ onConfirm, onClose, autoDate, setAutoDate }) {
|
||||
const sigRef = useRef(null);
|
||||
|
||||
const styles = {
|
||||
sigContainer: {
|
||||
display: "flex",
|
||||
justifyContent: "center",
|
||||
},
|
||||
sigBlock: {
|
||||
display: "inline-block",
|
||||
border: `1px solid ${primary45}`,
|
||||
},
|
||||
instructions: {
|
||||
display: "flex",
|
||||
justifyContent: "space-between",
|
||||
textAlign: "center",
|
||||
color: primary45,
|
||||
marginTop: 8,
|
||||
width: 600,
|
||||
alignSelf: "center",
|
||||
},
|
||||
instructionsContainer: {
|
||||
display: "flex",
|
||||
justifyContent: "center",
|
||||
},
|
||||
};
|
||||
return (
|
||||
<Dialog
|
||||
isVisible={true}
|
||||
title={"Add signature"}
|
||||
body={
|
||||
<div style={styles.container}>
|
||||
<div style={styles.sigContainer}>
|
||||
<div style={styles.sigBlock}>
|
||||
<SignatureCanvas
|
||||
velocityFilterWeight={1}
|
||||
ref={sigRef}
|
||||
canvasProps={{
|
||||
width: "600",
|
||||
height: 200,
|
||||
className: "sigCanvas",
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div style={styles.instructionsContainer}>
|
||||
<div style={styles.instructions}>
|
||||
<div>
|
||||
Auto date/time{" "}
|
||||
<input
|
||||
type={"checkbox"}
|
||||
checked={autoDate}
|
||||
onChange={(e) => setAutoDate(e.target.checked)}
|
||||
/>
|
||||
</div>
|
||||
<div>Draw your signature above</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<ConfirmOrCancel
|
||||
onCancel={onClose}
|
||||
onConfirm={() => {
|
||||
const sigURL = sigRef.current.toDataURL();
|
||||
onConfirm(sigURL);
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
}
|
||||
/>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,81 @@
|
||||
import React from "react";
|
||||
import { primary45 } from "../utils/colors";
|
||||
import useHover from "../hooks/useHover";
|
||||
|
||||
export function BigButton({
|
||||
title,
|
||||
onClick,
|
||||
inverted,
|
||||
fullWidth,
|
||||
customFillColor,
|
||||
customWhiteColor,
|
||||
style,
|
||||
noHover,
|
||||
id,
|
||||
small,
|
||||
disabled,
|
||||
marginRight,
|
||||
}) {
|
||||
const [hoverRef, isHovered] = useHover();
|
||||
|
||||
let fillColor = customFillColor || primary45;
|
||||
const whiteColor = customWhiteColor || "#FFF";
|
||||
|
||||
let initialBg = null;
|
||||
let hoverBg = fillColor;
|
||||
|
||||
let initialColor = fillColor;
|
||||
let hoverColor = whiteColor;
|
||||
|
||||
if (inverted) {
|
||||
initialBg = fillColor;
|
||||
hoverBg = null;
|
||||
initialColor = whiteColor;
|
||||
hoverColor = fillColor;
|
||||
}
|
||||
|
||||
if (disabled) {
|
||||
initialBg = "#ddd";
|
||||
hoverBg = "#ddd";
|
||||
fillColor = "#ddd";
|
||||
}
|
||||
|
||||
const styles = {
|
||||
container: {
|
||||
display: "inline-flex",
|
||||
alignItems: "center",
|
||||
justifyContent: "center",
|
||||
width: fullWidth ? "100%" : null,
|
||||
backgroundColor: isHovered && !noHover ? hoverBg : initialBg,
|
||||
color:
|
||||
isHovered && !noHover && !disabled
|
||||
? hoverColor
|
||||
: disabled
|
||||
? "#999"
|
||||
: initialColor,
|
||||
borderRadius: 4,
|
||||
padding: small ? "2px 4px" : "6px 8px",
|
||||
fontSize: small ? 14 : null,
|
||||
border: `1px solid ${fillColor}`,
|
||||
cursor: !disabled ? "pointer" : null,
|
||||
userSelect: "none",
|
||||
boxSizing: "border-box",
|
||||
marginRight,
|
||||
},
|
||||
};
|
||||
|
||||
return (
|
||||
<div
|
||||
id={id}
|
||||
ref={hoverRef}
|
||||
style={{ ...styles.container, ...style }}
|
||||
onClick={() => {
|
||||
if (!disabled) {
|
||||
onClick();
|
||||
}
|
||||
}}
|
||||
>
|
||||
{title}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,37 @@
|
||||
import { BigButton } from "./BigButton";
|
||||
import React from "react";
|
||||
|
||||
export function ConfirmOrCancel({
|
||||
onCancel,
|
||||
onConfirm,
|
||||
confirmTitle = "Confirm",
|
||||
leftBlock,
|
||||
hideCancel,
|
||||
disabled
|
||||
}) {
|
||||
const styles = {
|
||||
actions: {
|
||||
display: "flex",
|
||||
justifyContent: "space-between",
|
||||
},
|
||||
cancel: {
|
||||
marginRight: 8,
|
||||
},
|
||||
};
|
||||
|
||||
return (
|
||||
<div style={styles.actions}>
|
||||
<div>{leftBlock}</div>
|
||||
<div>
|
||||
{!hideCancel ? (
|
||||
<BigButton
|
||||
title={"Cancel"}
|
||||
style={styles.cancel}
|
||||
onClick={onCancel}
|
||||
/>
|
||||
) : null}
|
||||
<BigButton title={confirmTitle} inverted={true} onClick={onConfirm} disabled={disabled}/>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,56 @@
|
||||
import React from 'react';
|
||||
import {primary45} from '../utils/colors';
|
||||
import {FaTimes} from 'react-icons/fa';
|
||||
import {Modal} from './Modal';
|
||||
|
||||
export function Dialog({
|
||||
isVisible,
|
||||
body,
|
||||
onClose,
|
||||
title,
|
||||
noPadding,
|
||||
backgroundColor,
|
||||
positionTop,
|
||||
style,
|
||||
}) {
|
||||
if (!isVisible) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const styles = {
|
||||
header: {
|
||||
backgroundColor: primary45,
|
||||
color: '#FFF',
|
||||
padding: 8,
|
||||
fontSize: 14,
|
||||
display: 'flex',
|
||||
justifyContent: 'space-between',
|
||||
alignItems: 'center',
|
||||
},
|
||||
body: {
|
||||
padding: noPadding ? 0 : 14,
|
||||
backgroundColor: backgroundColor ? backgroundColor : '#FFF',
|
||||
},
|
||||
xIcon: {
|
||||
cursor: 'pointer',
|
||||
},
|
||||
};
|
||||
|
||||
return (
|
||||
<Modal onClose={onClose} isVisible={isVisible} positionTop={positionTop} style={style}>
|
||||
<div style={styles.container}>
|
||||
<div style={styles.header}>
|
||||
<div>{title}</div>
|
||||
<FaTimes
|
||||
color={'#FFF'}
|
||||
size={16}
|
||||
style={styles.xIcon}
|
||||
className={'dialogClose'}
|
||||
onClick={onClose}
|
||||
/>
|
||||
</div>
|
||||
<div style={styles.body}>{body}</div>
|
||||
</div>
|
||||
</Modal>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,37 @@
|
||||
import Draggable from "react-draggable";
|
||||
import {BigButton} from "./BigButton"; // The default
|
||||
import {FaCheck, FaTimes} from 'react-icons/fa'
|
||||
import {cleanBorder, errorColor, goodColor, primary45} from "../utils/colors";
|
||||
|
||||
export default function DraggableSignature({ url, onEnd, onSet, onCancel }) {
|
||||
const styles = {
|
||||
container: {
|
||||
position: 'absolute',
|
||||
zIndex: 100000,
|
||||
border: `2px solid ${primary45}`,
|
||||
},
|
||||
controls: {
|
||||
position: 'absolute',
|
||||
right: 0,
|
||||
display: 'inline-block',
|
||||
backgroundColor: primary45,
|
||||
// borderRadius: 4,
|
||||
},
|
||||
smallButton: {
|
||||
display: 'inline-block',
|
||||
cursor: 'pointer',
|
||||
padding: 4,
|
||||
}
|
||||
}
|
||||
return (
|
||||
<Draggable onStop={onEnd}>
|
||||
<div style={styles.container}>
|
||||
<div style={styles.controls}>
|
||||
<div style={styles.smallButton} onClick={onSet}><FaCheck color={goodColor}/></div>
|
||||
<div style={styles.smallButton} onClick={onCancel}><FaTimes color={errorColor}/></div>
|
||||
</div>
|
||||
<img src={url} width={200} style={styles.img} draggable={false} />
|
||||
</div>
|
||||
</Draggable>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,66 @@
|
||||
import Draggable from "react-draggable";
|
||||
import { FaCheck, FaTimes } from "react-icons/fa";
|
||||
import { cleanBorder, errorColor, goodColor, primary45 } from "../utils/colors";
|
||||
import { useState, useEffect, useRef } from "react";
|
||||
|
||||
export default function DraggableText({ onEnd, onSet, onCancel, initialText }) {
|
||||
const [text, setText] = useState("Text");
|
||||
const inputRef = useRef(null);
|
||||
|
||||
useEffect(() => {
|
||||
if (initialText) {
|
||||
setText(initialText)
|
||||
} else {
|
||||
inputRef.current.focus();
|
||||
inputRef.current.select()
|
||||
}
|
||||
}, [])
|
||||
|
||||
const styles = {
|
||||
container: {
|
||||
position: "absolute",
|
||||
zIndex: 100000,
|
||||
border: `2px solid ${primary45}`,
|
||||
},
|
||||
controls: {
|
||||
position: "absolute",
|
||||
right: 0,
|
||||
display: "inline-block",
|
||||
backgroundColor: primary45,
|
||||
// borderRadius: 4,
|
||||
},
|
||||
smallButton: {
|
||||
display: "inline-block",
|
||||
cursor: "pointer",
|
||||
padding: 4,
|
||||
},
|
||||
input: {
|
||||
border: 0,
|
||||
fontSize: 20,
|
||||
padding: 3,
|
||||
backgroundColor: 'rgba(0,0,0,0)',
|
||||
cursor: 'move'
|
||||
}
|
||||
};
|
||||
return (
|
||||
<Draggable onStop={onEnd}>
|
||||
<div style={styles.container}>
|
||||
<div style={styles.controls}>
|
||||
<div style={styles.smallButton} onClick={()=>onSet(text)}>
|
||||
<FaCheck color={goodColor} />
|
||||
</div>
|
||||
<div style={styles.smallButton} onClick={onCancel}>
|
||||
<FaTimes color={errorColor} />
|
||||
</div>
|
||||
</div>
|
||||
<input
|
||||
ref={inputRef}
|
||||
style={styles.input}
|
||||
value={text}
|
||||
placeholder={'Text'}
|
||||
onChange={(e) => setText(e.target.value)}
|
||||
/>
|
||||
</div>
|
||||
</Draggable>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,43 @@
|
||||
import React from 'react';
|
||||
import {primary45} from '../utils/colors';
|
||||
import {useIsSmallScreen} from '../hooks/useIsSmallScreen';
|
||||
|
||||
export function Modal({onClose, children, isVisible, style, positionTop}) {
|
||||
const isSmallScreen = useIsSmallScreen();
|
||||
|
||||
const styles = {
|
||||
container: {
|
||||
position: isSmallScreen ? 'fixed' : 'absolute',
|
||||
backgroundColor: '#FFF',
|
||||
border: `1px solid ${primary45}`,
|
||||
borderRadius: 4,
|
||||
top: positionTop ? positionTop : isSmallScreen ? 60 : 150,
|
||||
left: '50%',
|
||||
transform: 'translateX(-50%)',
|
||||
width: '94%',
|
||||
fontFamily: 'Open Sans',
|
||||
zIndex: 10000,
|
||||
boxShadow: '0 0px 14px hsla(0, 0%, 0%, 0.2)',
|
||||
},
|
||||
background: {
|
||||
position: 'fixed',
|
||||
width: '100%',
|
||||
height: '100%',
|
||||
top: 0,
|
||||
left: 0,
|
||||
backgroundColor: '#00000033',
|
||||
zIndex: 5000,
|
||||
},
|
||||
};
|
||||
|
||||
if (!isVisible) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<div style={styles.outer}>
|
||||
<div style={styles.background} onClick={onClose} />
|
||||
<div style={{...styles.container, ...style}}>{children}</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,40 @@
|
||||
import { BigButton } from "./BigButton";
|
||||
import {primary45} from "../utils/colors";
|
||||
|
||||
export default function PagingControl({totalPages, pageNum, setPageNum}) {
|
||||
const styles= {
|
||||
container: {
|
||||
marginTop: 8,
|
||||
marginBottom: 8,
|
||||
},
|
||||
inlineFlex: {
|
||||
display: 'flex',
|
||||
justifyContent: 'center',
|
||||
alignItems: 'center'
|
||||
},
|
||||
pageInfo: {
|
||||
padding: 8,
|
||||
color: primary45,
|
||||
fontSize: 14,
|
||||
}
|
||||
}
|
||||
return (
|
||||
<div style={styles.container}>
|
||||
<div style={styles.inlineFlex}>
|
||||
<BigButton
|
||||
title={"<"}
|
||||
onClick={() => setPageNum(pageNum - 1)}
|
||||
disabled={pageNum-1===-1}
|
||||
/>
|
||||
<div style={styles.pageInfo}>
|
||||
Page: {pageNum + 1}/{totalPages}
|
||||
</div>
|
||||
<BigButton
|
||||
title={">"}
|
||||
onClick={() => setPageNum(pageNum + 1)}
|
||||
disabled={pageNum+1>totalPages-1}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,29 @@
|
||||
import React, {useCallback, useRef, useState} from 'react';
|
||||
|
||||
export default function useHover() {
|
||||
const [value, setValue] = useState(false);
|
||||
|
||||
const handleMouseOver = useCallback(() => setValue(true), []);
|
||||
const handleMouseOut = useCallback(() => setValue(false), []);
|
||||
|
||||
const ref = useRef();
|
||||
|
||||
const callbackRef = useCallback(
|
||||
(node) => {
|
||||
if (ref.current) {
|
||||
ref.current.removeEventListener('mouseenter', handleMouseOver);
|
||||
ref.current.removeEventListener('mouseleave', handleMouseOut);
|
||||
}
|
||||
|
||||
ref.current = node;
|
||||
|
||||
if (ref.current) {
|
||||
ref.current.addEventListener('mouseenter', handleMouseOver);
|
||||
ref.current.addEventListener('mouseleave', handleMouseOut);
|
||||
}
|
||||
},
|
||||
[handleMouseOver, handleMouseOut],
|
||||
);
|
||||
|
||||
return [callbackRef, value];
|
||||
}
|
||||
@@ -0,0 +1,6 @@
|
||||
import {useWindowSize} from './useWindowSize';
|
||||
|
||||
export function useIsSmallScreen() {
|
||||
const windowSize = useWindowSize();
|
||||
return windowSize.width < 600;
|
||||
}
|
||||
@@ -0,0 +1,29 @@
|
||||
import React, {useState, useEffect} from 'react';
|
||||
|
||||
export function useWindowSize() {
|
||||
const isClient = typeof window === 'object';
|
||||
|
||||
function getSize() {
|
||||
return {
|
||||
width: isClient ? window.innerWidth : undefined,
|
||||
height: isClient ? window.innerHeight : undefined,
|
||||
};
|
||||
}
|
||||
|
||||
const [windowSize, setWindowSize] = useState(getSize);
|
||||
|
||||
useEffect(() => {
|
||||
if (!isClient) {
|
||||
return false;
|
||||
}
|
||||
|
||||
function handleResize() {
|
||||
setWindowSize(getSize());
|
||||
}
|
||||
|
||||
window.addEventListener('resize', handleResize);
|
||||
return () => window.removeEventListener('resize', handleResize);
|
||||
}, []); // Empty array ensures that effect is only run on mount and unmount
|
||||
|
||||
return windowSize;
|
||||
}
|
||||
13
EnvelopeGenerator.Web/Client/receiver-ui-react/src/index.css
Normal file
13
EnvelopeGenerator.Web/Client/receiver-ui-react/src/index.css
Normal file
@@ -0,0 +1,13 @@
|
||||
body {
|
||||
margin: 0;
|
||||
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Oxygen',
|
||||
'Ubuntu', 'Cantarell', 'Fira Sans', 'Droid Sans', 'Helvetica Neue',
|
||||
sans-serif;
|
||||
-webkit-font-smoothing: antialiased;
|
||||
-moz-osx-font-smoothing: grayscale;
|
||||
}
|
||||
|
||||
code {
|
||||
font-family: source-code-pro, Menlo, Monaco, Consolas, 'Courier New',
|
||||
monospace;
|
||||
}
|
||||
17
EnvelopeGenerator.Web/Client/receiver-ui-react/src/index.js
Normal file
17
EnvelopeGenerator.Web/Client/receiver-ui-react/src/index.js
Normal file
@@ -0,0 +1,17 @@
|
||||
import React from 'react';
|
||||
import ReactDOM from 'react-dom';
|
||||
import './index.css';
|
||||
import App from './App';
|
||||
import reportWebVitals from './reportWebVitals';
|
||||
|
||||
ReactDOM.render(
|
||||
<React.StrictMode>
|
||||
<App />
|
||||
</React.StrictMode>,
|
||||
document.getElementById('root')
|
||||
);
|
||||
|
||||
// If you want to start measuring performance in your app, pass a function
|
||||
// to log results (for example: reportWebVitals(console.log))
|
||||
// or send to an analytics endpoint. Learn more: https://bit.ly/CRA-vitals
|
||||
reportWebVitals();
|
||||
@@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 841.9 595.3"><g fill="#61DAFB"><path d="M666.3 296.5c0-32.5-40.7-63.3-103.1-82.4 14.4-63.6 8-114.2-20.2-130.4-6.5-3.8-14.1-5.6-22.4-5.6v22.3c4.6 0 8.3.9 11.4 2.6 13.6 7.8 19.5 37.5 14.9 75.7-1.1 9.4-2.9 19.3-5.1 29.4-19.6-4.8-41-8.5-63.5-10.9-13.5-18.5-27.5-35.3-41.6-50 32.6-30.3 63.2-46.9 84-46.9V78c-27.5 0-63.5 19.6-99.9 53.6-36.4-33.8-72.4-53.2-99.9-53.2v22.3c20.7 0 51.4 16.5 84 46.6-14 14.7-28 31.4-41.3 49.9-22.6 2.4-44 6.1-63.6 11-2.3-10-4-19.7-5.2-29-4.7-38.2 1.1-67.9 14.6-75.8 3-1.8 6.9-2.6 11.5-2.6V78.5c-8.4 0-16 1.8-22.6 5.6-28.1 16.2-34.4 66.7-19.9 130.1-62.2 19.2-102.7 49.9-102.7 82.3 0 32.5 40.7 63.3 103.1 82.4-14.4 63.6-8 114.2 20.2 130.4 6.5 3.8 14.1 5.6 22.5 5.6 27.5 0 63.5-19.6 99.9-53.6 36.4 33.8 72.4 53.2 99.9 53.2 8.4 0 16-1.8 22.6-5.6 28.1-16.2 34.4-66.7 19.9-130.1 62-19.1 102.5-49.9 102.5-82.3zm-130.2-66.7c-3.7 12.9-8.3 26.2-13.5 39.5-4.1-8-8.4-16-13.1-24-4.6-8-9.5-15.8-14.4-23.4 14.2 2.1 27.9 4.7 41 7.9zm-45.8 106.5c-7.8 13.5-15.8 26.3-24.1 38.2-14.9 1.3-30 2-45.2 2-15.1 0-30.2-.7-45-1.9-8.3-11.9-16.4-24.6-24.2-38-7.6-13.1-14.5-26.4-20.8-39.8 6.2-13.4 13.2-26.8 20.7-39.9 7.8-13.5 15.8-26.3 24.1-38.2 14.9-1.3 30-2 45.2-2 15.1 0 30.2.7 45 1.9 8.3 11.9 16.4 24.6 24.2 38 7.6 13.1 14.5 26.4 20.8 39.8-6.3 13.4-13.2 26.8-20.7 39.9zm32.3-13c5.4 13.4 10 26.8 13.8 39.8-13.1 3.2-26.9 5.9-41.2 8 4.9-7.7 9.8-15.6 14.4-23.7 4.6-8 8.9-16.1 13-24.1zM421.2 430c-9.3-9.6-18.6-20.3-27.8-32 9 .4 18.2.7 27.5.7 9.4 0 18.7-.2 27.8-.7-9 11.7-18.3 22.4-27.5 32zm-74.4-58.9c-14.2-2.1-27.9-4.7-41-7.9 3.7-12.9 8.3-26.2 13.5-39.5 4.1 8 8.4 16 13.1 24 4.7 8 9.5 15.8 14.4 23.4zM420.7 163c9.3 9.6 18.6 20.3 27.8 32-9-.4-18.2-.7-27.5-.7-9.4 0-18.7.2-27.8.7 9-11.7 18.3-22.4 27.5-32zm-74 58.9c-4.9 7.7-9.8 15.6-14.4 23.7-4.6 8-8.9 16-13 24-5.4-13.4-10-26.8-13.8-39.8 13.1-3.1 26.9-5.8 41.2-7.9zm-90.5 125.2c-35.4-15.1-58.3-34.9-58.3-50.6 0-15.7 22.9-35.6 58.3-50.6 8.6-3.7 18-7 27.7-10.1 5.7 19.6 13.2 40 22.5 60.9-9.2 20.8-16.6 41.1-22.2 60.6-9.9-3.1-19.3-6.5-28-10.2zM310 490c-13.6-7.8-19.5-37.5-14.9-75.7 1.1-9.4 2.9-19.3 5.1-29.4 19.6 4.8 41 8.5 63.5 10.9 13.5 18.5 27.5 35.3 41.6 50-32.6 30.3-63.2 46.9-84 46.9-4.5-.1-8.3-1-11.3-2.7zm237.2-76.2c4.7 38.2-1.1 67.9-14.6 75.8-3 1.8-6.9 2.6-11.5 2.6-20.7 0-51.4-16.5-84-46.6 14-14.7 28-31.4 41.3-49.9 22.6-2.4 44-6.1 63.6-11 2.3 10.1 4.1 19.8 5.2 29.1zm38.5-66.7c-8.6 3.7-18 7-27.7 10.1-5.7-19.6-13.2-40-22.5-60.9 9.2-20.8 16.6-41.1 22.2-60.6 9.9 3.1 19.3 6.5 28.1 10.2 35.4 15.1 58.3 34.9 58.3 50.6-.1 15.7-23 35.6-58.4 50.6zM320.8 78.4z"/><circle cx="420.9" cy="296.5" r="45.7"/><path d="M520.5 78.1z"/></g></svg>
|
||||
|
After Width: | Height: | Size: 2.6 KiB |
@@ -0,0 +1,13 @@
|
||||
const reportWebVitals = onPerfEntry => {
|
||||
if (onPerfEntry && onPerfEntry instanceof Function) {
|
||||
import('web-vitals').then(({ getCLS, getFID, getFCP, getLCP, getTTFB }) => {
|
||||
getCLS(onPerfEntry);
|
||||
getFID(onPerfEntry);
|
||||
getFCP(onPerfEntry);
|
||||
getLCP(onPerfEntry);
|
||||
getTTFB(onPerfEntry);
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
export default reportWebVitals;
|
||||
@@ -0,0 +1,5 @@
|
||||
// jest-dom adds custom jest matchers for asserting on DOM nodes.
|
||||
// allows you to do things like:
|
||||
// expect(element).toHaveTextContent(/react/i)
|
||||
// learn more: https://github.com/testing-library/jest-dom
|
||||
import '@testing-library/jest-dom';
|
||||
@@ -0,0 +1,27 @@
|
||||
export function blobToURL(blob) {
|
||||
return new Promise((resolve) => {
|
||||
const reader = new FileReader();
|
||||
reader.readAsDataURL(blob);
|
||||
reader.onloadend = function () {
|
||||
const base64data = reader.result;
|
||||
resolve(base64data);
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
export async function fileToBlob(file, handleUpdate) {
|
||||
const { content, size } = file;
|
||||
let chunks = [];
|
||||
let i = 0;
|
||||
const totalCount = Math.round(size / 250000);
|
||||
|
||||
for await (const chunk of content) {
|
||||
if (handleUpdate) {
|
||||
handleUpdate(i, totalCount);
|
||||
}
|
||||
chunks.push(chunk);
|
||||
i++;
|
||||
}
|
||||
// eslint-disable-next-line no-undef
|
||||
return new Blob(chunks);
|
||||
}
|
||||
@@ -0,0 +1,28 @@
|
||||
export const primary = '#2b6284';
|
||||
export const primary2 = '#ecf4f9';
|
||||
export const primary3 = '#9fc7e0';
|
||||
export const primary35 = '#97bace';
|
||||
export const primary4 = 'hsl(204,38%,55%)';
|
||||
export const primary45 = 'hsl(218,49%,66%)';
|
||||
export const primary46 = '#6778cb';
|
||||
|
||||
export const primary15 = 'rgb(241 249 255)';
|
||||
|
||||
export const primary5 = '#3881ad';
|
||||
export const primary6 = '#132b3a';
|
||||
|
||||
// export const primary = '#666';
|
||||
// export const primary2 = '#EEE';
|
||||
// export const primary3 = '#CCC';
|
||||
// export const primary4 = '#AAA';
|
||||
// export const primary5 = '#888';
|
||||
// export const primary6 = '#333';
|
||||
|
||||
export const primary16 = 'hsl(208 100% 96% / 1)';
|
||||
|
||||
export const errorColor = '#ef6565';
|
||||
export const lightErrorColor = '#ef9c9c';
|
||||
export const goodColor = '#53c171';
|
||||
|
||||
export const cleanBorder = '1px solid rgb(208, 227, 239)';
|
||||
export const lightBorder = 'hsl(203 51% 80% / 1)';
|
||||
11620
EnvelopeGenerator.Web/Client/receiver-ui-react/yarn.lock
Normal file
11620
EnvelopeGenerator.Web/Client/receiver-ui-react/yarn.lock
Normal file
File diff suppressed because it is too large
Load Diff
@@ -4,6 +4,7 @@ using EnvelopeGenerator.Application.Common.Extensions;
|
||||
using EnvelopeGenerator.Application.Common.Interfaces.Services;
|
||||
using EnvelopeGenerator.Application.Common.Notifications.DocSigned;
|
||||
using EnvelopeGenerator.Application.EnvelopeReceivers.Queries;
|
||||
using EnvelopeGenerator.Application.Histories.Queries;
|
||||
using EnvelopeGenerator.Domain.Constants;
|
||||
using EnvelopeGenerator.Web.Extensions;
|
||||
using MediatR;
|
||||
@@ -11,7 +12,6 @@ using Microsoft.AspNetCore.Authentication;
|
||||
using Microsoft.AspNetCore.Authentication.Cookies;
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using System.Dynamic;
|
||||
|
||||
namespace EnvelopeGenerator.Web.Controllers;
|
||||
|
||||
@@ -58,7 +58,9 @@ public class AnnotationController : ControllerBase
|
||||
|
||||
// Again check if receiver has already signed
|
||||
if (await _mediator.IsSignedAsync(uuid, signature, cancel))
|
||||
return Problem(statusCode: 403);
|
||||
return Problem(statusCode: 409);
|
||||
else if (await _mediator.AnyHistoryAsync(uuid, new[] { EnvelopeStatus.EnvelopeRejected, EnvelopeStatus.DocumentRejected }, cancel))
|
||||
return Problem(statusCode: 423);
|
||||
|
||||
var docSignedNotification = await _mediator
|
||||
.ReadEnvelopeReceiverAsync(uuid, signature, cancel)
|
||||
@@ -71,7 +73,7 @@ public class AnnotationController : ControllerBase
|
||||
|
||||
return Ok();
|
||||
}
|
||||
|
||||
|
||||
[Authorize(Roles = ReceiverRole.FullyAuth)]
|
||||
[HttpPost("reject")]
|
||||
[Obsolete("Use DigitalData.Core.Exceptions and .Middleware")]
|
||||
|
||||
@@ -95,6 +95,23 @@ public class EnvelopeController : ViewControllerBase
|
||||
}
|
||||
#endregion
|
||||
|
||||
#region UseAccessCode
|
||||
if (!er.Envelope!.UseAccessCode)
|
||||
{
|
||||
(string? uuid, string? signature) = decoded.ParseEnvelopeReceiverId();
|
||||
var er_secret_res = await _envRcvService.ReadWithSecretByUuidSignatureAsync(uuid: uuid!, signature: signature!);
|
||||
|
||||
if (er_secret_res.IsFailed)
|
||||
{
|
||||
_logger.LogNotice(er_secret_res.Notices);
|
||||
return this.ViewEnvelopeNotFound();
|
||||
}
|
||||
var er_secret = er_secret_res.Data;
|
||||
await HttpContext.SignInEnvelopeAsync(er_secret, ReceiverRole.FullyAuth);
|
||||
return await CreateShowEnvelopeView(er_secret);
|
||||
}
|
||||
#endregion UseAccessCode
|
||||
|
||||
#region Send Access Code
|
||||
bool accessCodeAlreadyRequested = await _historyService.AccessCodeAlreadyRequested(envelopeId: er.Envelope!.Id, userReference: er.Receiver!.EmailAddress);
|
||||
if (!accessCodeAlreadyRequested)
|
||||
@@ -121,7 +138,7 @@ public class EnvelopeController : ViewControllerBase
|
||||
|
||||
[HttpPost("{envelopeReceiverId}")]
|
||||
[Obsolete("Use MediatR")]
|
||||
public async Task<IActionResult> LogInEnvelope([FromRoute] string envelopeReceiverId, [FromForm] Auth auth)
|
||||
public async Task<IActionResult> LogInEnvelope([FromRoute] string envelopeReceiverId, [FromForm] Auth auth, CancellationToken cancel)
|
||||
{
|
||||
try
|
||||
{
|
||||
@@ -145,6 +162,15 @@ public class EnvelopeController : ViewControllerBase
|
||||
}
|
||||
var er_secret = er_secret_res.Data;
|
||||
|
||||
//check rejection
|
||||
var rejRcvrs = await _historyService.ReadRejectingReceivers(er_secret.Envelope!.Id);
|
||||
if (rejRcvrs.Any())
|
||||
{
|
||||
await HttpContext.SignOutAsync(CookieAuthenticationDefaults.AuthenticationScheme);
|
||||
ViewBag.IsExt = !rejRcvrs.Contains(er_secret.Receiver); //external if the current user is not rejected
|
||||
return View("EnvelopeRejected", er_secret);
|
||||
}
|
||||
|
||||
// show envelope if already logged in
|
||||
if (User.IsInRole(ReceiverRole.FullyAuth))
|
||||
return await CreateShowEnvelopeView(er_secret);
|
||||
@@ -190,7 +216,7 @@ public class EnvelopeController : ViewControllerBase
|
||||
return this.ViewInnerServiceError();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
private async Task<IActionResult> CreateEnvelopeLockedView(EnvelopeReceiverDto er, CancellationToken cancel)
|
||||
{
|
||||
var uuidClaim = User.GetAuthEnvelopeUuid();
|
||||
|
||||
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);
|
||||
}
|
||||
}
|
||||
@@ -1,7 +1,7 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk.Web">
|
||||
|
||||
<PropertyGroup>
|
||||
<TargetFramework>net7.0</TargetFramework>
|
||||
<TargetFrameworks>net7.0;net8.0;net9.0</TargetFrameworks>
|
||||
<Nullable>enable</Nullable>
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
<PackageId>EnvelopeGenerator.Web</PackageId>
|
||||
@@ -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.4.1</Version>
|
||||
<AssemblyVersion>3.4.1</AssemblyVersion>
|
||||
<FileVersion>3.4.1</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>
|
||||
|
||||
@@ -2094,20 +2094,13 @@
|
||||
<None Include="wwwroot\lib\bootstrap-icons\icons\zoom-out.svg" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="AutoMapper" Version="13.0.1" />
|
||||
<ItemGroup Condition="'$(TargetFramework)' == 'net7.0'">
|
||||
<PackageReference Include="BuildBundlerMinifier2022" Version="2.9.9" />
|
||||
<PackageReference Include="DigitalData.Core.API" Version="2.2.1" />
|
||||
<PackageReference Include="DigitalData.Core.Exceptions" Version="1.1.0" />
|
||||
<PackageReference Include="DigitalData.EmailProfilerDispatcher" Version="3.1.1" />
|
||||
<PackageReference Include="HtmlSanitizer" Version="8.0.865" />
|
||||
<PackageReference Include="Microsoft.AspNetCore.OpenApi" Version="7.0.4" />
|
||||
<PackageReference Include="Microsoft.EntityFrameworkCore" Version="7.0.20" />
|
||||
<PackageReference Include="Microsoft.EntityFrameworkCore.Design" Version="7.0.15">
|
||||
<PrivateAssets>all</PrivateAssets>
|
||||
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
|
||||
</PackageReference>
|
||||
<PackageReference Include="Microsoft.EntityFrameworkCore.SqlServer" Version="7.0.20" />
|
||||
<PackageReference Include="Microsoft.Extensions.Caching.SqlServer" Version="7.0.20" />
|
||||
<PackageReference Include="Newtonsoft.Json" Version="13.0.3" />
|
||||
<PackageReference Include="NLog" Version="5.2.5" />
|
||||
@@ -2126,6 +2119,56 @@
|
||||
<PackageReference Include="System.Security.Cryptography.Cng" Version="5.0.0" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup Condition="'$(TargetFramework)' == 'net8.0'">
|
||||
<PackageReference Include="BuildBundlerMinifier2022" Version="2.9.9" />
|
||||
<PackageReference Include="DigitalData.Core.API" Version="2.2.1" />
|
||||
<PackageReference Include="DigitalData.Core.Exceptions" Version="1.1.0" />
|
||||
<PackageReference Include="DigitalData.EmailProfilerDispatcher" Version="3.1.1" />
|
||||
<PackageReference Include="HtmlSanitizer" Version="8.0.865" />
|
||||
<PackageReference Include="Microsoft.AspNetCore.OpenApi" Version="7.0.4" />
|
||||
<PackageReference Include="Microsoft.Extensions.Caching.SqlServer" Version="7.0.20" />
|
||||
<PackageReference Include="Newtonsoft.Json" Version="13.0.3" />
|
||||
<PackageReference Include="NLog" Version="5.2.5" />
|
||||
<PackageReference Include="NLog.Web.AspNetCore" Version="5.3.0" />
|
||||
<PackageReference Include="Quartz" Version="3.8.0" />
|
||||
<PackageReference Include="Quartz.AspNetCore" Version="3.8.0" />
|
||||
<PackageReference Include="Quartz.Plugins" Version="3.8.0" />
|
||||
<PackageReference Include="Quartz.Serialization.Json" Version="3.8.0" />
|
||||
<PackageReference Include="Swashbuckle.AspNetCore" Version="10.0.1" />
|
||||
<PackageReference Include="System.Configuration.ConfigurationManager" Version="8.0.1" />
|
||||
<PackageReference Include="System.Diagnostics.PerformanceCounter" Version="7.0.0" />
|
||||
<PackageReference Include="System.DirectoryServices" Version="8.0.0" />
|
||||
<PackageReference Include="System.DirectoryServices.AccountManagement" Version="8.0.1" />
|
||||
<PackageReference Include="System.DirectoryServices.Protocols" Version="8.0.1" />
|
||||
<PackageReference Include="System.Drawing.Common" Version="8.0.16" />
|
||||
<PackageReference Include="System.Security.Cryptography.Cng" Version="5.0.0" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup Condition="'$(TargetFramework)' == 'net9.0'">
|
||||
<PackageReference Include="BuildBundlerMinifier2022" Version="2.9.9" />
|
||||
<PackageReference Include="DigitalData.Core.API" Version="2.2.1" />
|
||||
<PackageReference Include="DigitalData.Core.Exceptions" Version="1.1.0" />
|
||||
<PackageReference Include="DigitalData.EmailProfilerDispatcher" Version="3.1.1" />
|
||||
<PackageReference Include="HtmlSanitizer" Version="8.0.865" />
|
||||
<PackageReference Include="Microsoft.AspNetCore.OpenApi" Version="7.0.4" />
|
||||
<PackageReference Include="Microsoft.Extensions.Caching.SqlServer" Version="7.0.20" />
|
||||
<PackageReference Include="Newtonsoft.Json" Version="13.0.3" />
|
||||
<PackageReference Include="NLog" Version="5.2.5" />
|
||||
<PackageReference Include="NLog.Web.AspNetCore" Version="5.3.0" />
|
||||
<PackageReference Include="Quartz" Version="3.8.0" />
|
||||
<PackageReference Include="Quartz.AspNetCore" Version="3.8.0" />
|
||||
<PackageReference Include="Quartz.Plugins" Version="3.8.0" />
|
||||
<PackageReference Include="Quartz.Serialization.Json" Version="3.8.0" />
|
||||
<PackageReference Include="Swashbuckle.AspNetCore" Version="10.0.1" />
|
||||
<PackageReference Include="System.Configuration.ConfigurationManager" Version="9.0.11" />
|
||||
<PackageReference Include="System.Diagnostics.PerformanceCounter" Version="7.0.0" />
|
||||
<PackageReference Include="System.DirectoryServices" Version="9.0.4" />
|
||||
<PackageReference Include="System.DirectoryServices.AccountManagement" Version="9.0.4" />
|
||||
<PackageReference Include="System.DirectoryServices.Protocols" Version="9.0.4" />
|
||||
<PackageReference Include="System.Drawing.Common" Version="9.0.5" />
|
||||
<PackageReference Include="System.Security.Cryptography.Cng" Version="5.0.0" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\EnvelopeGenerator.Application\EnvelopeGenerator.Application.csproj" />
|
||||
<ProjectReference Include="..\EnvelopeGenerator.Infrastructure\EnvelopeGenerator.Infrastructure.csproj" />
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -11,12 +11,10 @@ https://go.microsoft.com/fwlink/?LinkID=208121.
|
||||
<LaunchSiteAfterPublish>true</LaunchSiteAfterPublish>
|
||||
<ExcludeApp_Data>false</ExcludeApp_Data>
|
||||
<ProjectGuid>5e0e17c0-ff5a-4246-bf87-1add85376a27</ProjectGuid>
|
||||
<DesktopBuildPackageLocation>P:\Install .Net\0 DD - Smart UP\signFLOW\Web\net9\win64\$(Version)\EnvelopeGenerator.Web.zip</DesktopBuildPackageLocation>
|
||||
<DesktopBuildPackageLocation>P:\Install .Net\0 DD - Smart UP\signFLOW\Web\net8\$(Version)\EnvelopeGenerator.Web.zip</DesktopBuildPackageLocation>
|
||||
<PackageAsSingleFile>true</PackageAsSingleFile>
|
||||
<DeployIisAppPath>EnvelopeGenerator</DeployIisAppPath>
|
||||
<_TargetId>IISWebDeployPackage</_TargetId>
|
||||
<TargetFramework>net9.0</TargetFramework>
|
||||
<RuntimeIdentifier>win-x64</RuntimeIdentifier>
|
||||
<SelfContained>true</SelfContained>
|
||||
<TargetFramework>net8.0</TargetFramework>
|
||||
</PropertyGroup>
|
||||
</Project>
|
||||
@@ -79,7 +79,7 @@
|
||||
}
|
||||
else
|
||||
{
|
||||
<h6>@($"{@envelope?.Message}")</h6>
|
||||
<div class="markdown">@(envelope?.Message)</div>
|
||||
}
|
||||
<p>
|
||||
<small class="text-body-secondary">
|
||||
|
||||
@@ -49,6 +49,7 @@
|
||||
<script src="~/lib/bootstrap/dist/js/bootstrap.min.js"></script>
|
||||
<script src="~/lib/sweetalert2/sweetalert2.min.js"></script>
|
||||
<script src="~/lib/alertifyjs/alertify.min.js"></script>
|
||||
<script src="~/lib/marked/marked.umd.min.js"></script>
|
||||
<script src="~/js/lazy.min.js" asp-append-version="true"></script>
|
||||
<script src="~/js/ui.min.js" asp-append-version="true"></script>
|
||||
<script src="~/js/annotation.js" asp-append-version="true"></script>
|
||||
@@ -95,5 +96,6 @@
|
||||
</div>
|
||||
<a href="/privacy-policy.@(_localizer.Culture()).html" target="_blank">@_localizer.Privacy()</a>
|
||||
</footer>
|
||||
<script src="~/js/markdown.min.js" asp-append-version="true"></script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -41,6 +41,12 @@
|
||||
"wwwroot/js/util.js"
|
||||
]
|
||||
},
|
||||
{
|
||||
"outputFileName": "wwwroot/js/markdown.min.js",
|
||||
"inputFiles": [
|
||||
"wwwroot/js/markdown.js"
|
||||
]
|
||||
},
|
||||
{
|
||||
"outputFileName": "wwwroot/css/error-space.min.css",
|
||||
"inputFiles": [
|
||||
|
||||
@@ -321,29 +321,43 @@ function mapSignature(iJSON) {
|
||||
// formFields
|
||||
...iJSON.formFieldValues.filter(field => !field.name.includes("label")).map((field) => {
|
||||
const nameParts = field.name.split('#');
|
||||
const [x, y, width, height] = iJSON.annotations.find(iAnnot => iAnnot.id === field.name).bbox;
|
||||
return {
|
||||
elementId: Number(nameParts[2]),
|
||||
name: nameParts[3],
|
||||
value: field.value,
|
||||
type: field.type
|
||||
type: field.type,
|
||||
x: x,
|
||||
y: y,
|
||||
width: width,
|
||||
height: height
|
||||
};
|
||||
}),
|
||||
|
||||
// frames
|
||||
...iJSON.annotations.filter(annot => annot.description === 'FRAME').map((annot) => {
|
||||
const preElement = findNearest(annot, e => e.bbox[0], e => e.bbox[1], ...iJSON.annotations.filter(field => field.id.includes("signature")));
|
||||
const preElement = findNearest(annot, e => e.bbox[0], e => e.bbox[1], iJSON.annotations.filter(
|
||||
field => field.id.includes("signature") && field.pageIndex === annot.pageIndex
|
||||
));
|
||||
const idPartsOfPre = preElement.id.split('#');
|
||||
const [x, y, width, height] = annot.bbox;
|
||||
return {
|
||||
elementId: Number(idPartsOfPre[2]),
|
||||
name: 'frame',
|
||||
value: fixBase64(iJSON.attachments[annot.imageAttachmentId]?.binary),
|
||||
type: annot.type
|
||||
type: annot.type,
|
||||
x: x,
|
||||
y: y,
|
||||
width: width,
|
||||
height: height
|
||||
};
|
||||
}),
|
||||
|
||||
// signatures
|
||||
...iJSON.annotations.filter(annot => annot.isSignature).map(annot => {
|
||||
const preElement = findNearest(annot, e => e.bbox[0], e => e.bbox[1], ...iJSON.annotations.filter(field => field.id.includes("signature")));
|
||||
const preElement = findNearest(annot, e => e.bbox[0], e => e.bbox[1], iJSON.annotations.filter(
|
||||
field => field.id.includes("signature") && field.pageIndex === annot.pageIndex
|
||||
));
|
||||
const idPartsOfPre = preElement.id.split('#');
|
||||
|
||||
let value;
|
||||
@@ -357,11 +371,16 @@ function mapSignature(iJSON) {
|
||||
else
|
||||
throw new Error("Signature mapping failed: The data structure from the third-party library is incompatible or missing required fields.");
|
||||
|
||||
const [x, y, width, height] = annot.bbox;
|
||||
return {
|
||||
elementId: Number(idPartsOfPre[2]),
|
||||
name: 'signature',
|
||||
value,
|
||||
type: annot.type
|
||||
type: annot.type,
|
||||
x: x,
|
||||
y: y,
|
||||
width: width,
|
||||
height: height
|
||||
};
|
||||
})
|
||||
];
|
||||
|
||||
@@ -255,7 +255,7 @@ class App {
|
||||
});
|
||||
|
||||
if (!res.ok) {
|
||||
if (res.status === 403) {
|
||||
if (res.status === 409) {
|
||||
Swal.fire({
|
||||
title: 'Warnung',
|
||||
text: 'Umschlag ist nicht mehr verfügbar.',
|
||||
@@ -263,6 +263,17 @@ class App {
|
||||
})
|
||||
return false
|
||||
}
|
||||
else if (res.status === 423) {
|
||||
Swal.fire({
|
||||
title: 'Info',
|
||||
text: 'Dokument wurde von einem Empfänger abgelehnt. Sie werden weitergeleitet...',
|
||||
icon: 'info',
|
||||
timer: 2000,
|
||||
showConfirmButton: false
|
||||
}).then(() => {
|
||||
location.reload();
|
||||
});
|
||||
}
|
||||
else {
|
||||
throw new Error()
|
||||
}
|
||||
|
||||
2
EnvelopeGenerator.Web/wwwroot/js/app.min.js
vendored
2
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(`
|
||||
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===403)return Swal.fire({title:"Warnung",text:"Umschlag ist nicht mehr verfügbar.",icon:"warning"}),!1;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(){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;}
|
||||
@@ -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`});
|
||||
11
EnvelopeGenerator.Web/wwwroot/js/markdown.js
Normal file
11
EnvelopeGenerator.Web/wwwroot/js/markdown.js
Normal file
@@ -0,0 +1,11 @@
|
||||
marked.use({
|
||||
async: true,
|
||||
breaks: true,
|
||||
gfm: true,
|
||||
});
|
||||
|
||||
(async () => {
|
||||
for (const el of document.querySelectorAll('.markdown')) {
|
||||
el.innerHTML = await marked.parse(el.textContent.replace(/(\t| )/g, " "));
|
||||
}
|
||||
})();
|
||||
1
EnvelopeGenerator.Web/wwwroot/js/markdown.min.js
vendored
Normal file
1
EnvelopeGenerator.Web/wwwroot/js/markdown.min.js
vendored
Normal file
@@ -0,0 +1 @@
|
||||
marked.use({"async":!0,breaks:!0,gfm:!0});(async()=>{for(const n of document.querySelectorAll(".markdown"))n.innerHTML=await marked.parse(n.textContent.replace(/(\t| )/g," "))})();
|
||||
@@ -14,7 +14,7 @@ function detailedCurrentDate() {
|
||||
}).format();
|
||||
}
|
||||
|
||||
function findNearest(origin, getX, getY, ...dests) {
|
||||
function findNearest(origin, getX, getY, dests) {
|
||||
const distanceToOrigin = (point) => Math.sqrt((getX(origin) - getX(point))**2 + (getY(origin) - getY(point))**2);
|
||||
return dests.reduce(
|
||||
(nearest, dest) => {
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user