Compare commits
155 Commits
3f116ce11a
...
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 | |||
| ef28bbaaf1 | |||
| 258de6244c | |||
| a845b85a5c | |||
| 02a7b706cf | |||
| 7912469709 | |||
| 75d975223e | |||
| c456d67d03 | |||
| 241e59fc7e | |||
| f0d101bb23 | |||
| 8db5afae40 | |||
| b62cca5961 | |||
| 0e7b120ded | |||
| d8cbdb0c65 | |||
| 0107602a84 | |||
| 02ecd88758 | |||
| 17c7e46388 | |||
| f3af30c67d | |||
| 90e10d3d04 | |||
| af14ef7ce5 | |||
| 1edcfed318 | |||
| 2004c7ced2 | |||
| 40135fb8a2 | |||
| b57c0aa9c7 | |||
| 2c4c18935f | |||
| d8ed06fdb6 | |||
| 09bf8db884 | |||
| 911c812b19 | |||
| 8ae0f79365 | |||
| 0ca54fe1fe | |||
| a1d6b5347f | |||
| 6cc631111c | |||
| 9d6074874f | |||
| 26bdb0806d | |||
| 7919f02ffd | |||
| 04ae14c660 | |||
| cff79730b0 | |||
| 188cb67306 | |||
| abaa315b24 | |||
| 4f463c27e6 | |||
| d6f17ec4e8 | |||
| e3e2831da1 | |||
| 52306d481f | |||
| f046be240b | |||
| 16e5d5c692 | |||
| e64ad44b71 | |||
| e88bd55198 | |||
| 4abed0e1bc | |||
| 69821e64c6 | |||
| f13a2434f7 | |||
| ecc7552951 | |||
| d10f19d92a | |||
| 5e53f2b691 | |||
| f56928f44f | |||
| faa37e0dcd | |||
| e51470a449 | |||
| adce61fead | |||
| e052bf56f4 | |||
| 22a7619627 | |||
| 281cf47834 | |||
| a258dcdad0 | |||
| 79c26eb5b5 | |||
| 1f745ae79c | |||
| ce7ca39c39 | |||
| 7b6f916486 | |||
| 57422a481c | |||
| e96523b786 | |||
| 3b7d0e1321 | |||
| 9adc1ea4e7 | |||
| 510f5e9ddd | |||
| 44b204ca68 | |||
| b72ac68daf | |||
| 7d85d59ace | |||
| 4fad41bd0b | |||
| 39936792aa | |||
| 74f444a8d6 | |||
| b67f26cc21 | |||
| a29f918125 |
1
.vscode/settings.json
vendored
Normal file
1
.vscode/settings.json
vendored
Normal file
@@ -0,0 +1 @@
|
||||
{}
|
||||
73
EnvelopeGenerator.Application/Common/Dto/AnnotationDto.cs
Normal file
73
EnvelopeGenerator.Application/Common/Dto/AnnotationDto.cs
Normal file
@@ -0,0 +1,73 @@
|
||||
namespace EnvelopeGenerator.Application.Common.Dto;
|
||||
|
||||
/// <summary>
|
||||
///
|
||||
/// </summary>
|
||||
public record AnnotationCreateDto
|
||||
{
|
||||
/// <summary>
|
||||
///
|
||||
/// </summary>
|
||||
public int ElementId { get; init; }
|
||||
|
||||
/// <summary>
|
||||
///
|
||||
/// </summary>
|
||||
public string Name { get; init; } = null!;
|
||||
|
||||
/// <summary>
|
||||
///
|
||||
/// </summary>
|
||||
public string Value { get; init; } = null!;
|
||||
|
||||
/// <summary>
|
||||
///
|
||||
/// </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>
|
||||
///
|
||||
/// </summary>
|
||||
public record AnnotationDto : AnnotationCreateDto
|
||||
{
|
||||
/// <summary>
|
||||
///
|
||||
/// </summary>
|
||||
public long Id { get; init; }
|
||||
|
||||
/// <summary>
|
||||
///
|
||||
/// </summary>
|
||||
public DateTime AddedWhen { get; init; }
|
||||
|
||||
/// <summary>
|
||||
///
|
||||
/// </summary>
|
||||
public DateTime? ChangedWhen { get; init; }
|
||||
|
||||
/// <summary>
|
||||
///
|
||||
/// </summary>
|
||||
public string? ChangedWho { get; init; }
|
||||
}
|
||||
@@ -82,7 +82,7 @@ public record EnvelopeDto
|
||||
/// <summary>
|
||||
///
|
||||
/// </summary>
|
||||
public bool? UseAccessCode { get; set; }
|
||||
public bool UseAccessCode { get; set; } = true;
|
||||
|
||||
/// <summary>
|
||||
///
|
||||
|
||||
@@ -35,6 +35,7 @@ public class MappingProfile : Profile
|
||||
CreateMap<EnvelopeType, EnvelopeTypeDto>();
|
||||
CreateMap<Domain.Entities.Receiver, ReceiverDto>();
|
||||
CreateMap<Domain.Entities.EnvelopeReceiverReadOnly, EnvelopeReceiverReadOnlyDto>();
|
||||
CreateMap<ElementAnnotation, AnnotationDto>();
|
||||
|
||||
// DTO to Entity mappings
|
||||
CreateMap<ConfigDto, Config>();
|
||||
@@ -50,6 +51,8 @@ public class MappingProfile : Profile
|
||||
CreateMap<ReceiverDto, Domain.Entities.Receiver>().ForMember(rcv => rcv.EnvelopeReceivers, rcvReadDto => rcvReadDto.Ignore());
|
||||
CreateMap<EnvelopeReceiverReadOnlyCreateDto, Domain.Entities.EnvelopeReceiverReadOnly>();
|
||||
CreateMap<EnvelopeReceiverReadOnlyUpdateDto, Domain.Entities.EnvelopeReceiverReadOnly>();
|
||||
CreateMap<AnnotationCreateDto, ElementAnnotation>()
|
||||
.ForMember(dest => dest.AddedWhen, opt => opt.MapFrom(_ => DateTime.UtcNow));
|
||||
|
||||
// Messaging mappings
|
||||
// for GTX messaging
|
||||
|
||||
@@ -1,12 +1,20 @@
|
||||
using EnvelopeGenerator.Application.Common.Dto.EnvelopeReceiver;
|
||||
using EnvelopeGenerator.Application.Common.Dto;
|
||||
using EnvelopeGenerator.Application.Common.Dto.EnvelopeReceiver;
|
||||
using EnvelopeGenerator.Application.Common.Extensions;
|
||||
using EnvelopeGenerator.Application.Common.Notifications.RemoveSignature;
|
||||
using EnvelopeGenerator.Domain.Constants;
|
||||
using MediatR;
|
||||
using Newtonsoft.Json;
|
||||
using System.Dynamic;
|
||||
|
||||
namespace EnvelopeGenerator.Application.Common.Notifications.DocSigned;
|
||||
|
||||
/// <summary>
|
||||
///
|
||||
/// </summary>
|
||||
/// <param name="Instant"></param>
|
||||
/// <param name="Structured"></param>
|
||||
public record PsPdfKitAnnotation(ExpandoObject Instant, IEnumerable<AnnotationCreateDto> Structured);
|
||||
|
||||
/// <summary>
|
||||
///
|
||||
/// </summary>
|
||||
@@ -16,7 +24,7 @@ public record DocSignedNotification(EnvelopeReceiverDto Original) : EnvelopeRece
|
||||
/// <summary>
|
||||
///
|
||||
/// </summary>
|
||||
public required ExpandoObject Annotations { get; init; }
|
||||
public PsPdfKitAnnotation PsPdfKitAnnotation { get; init; } = null!;
|
||||
|
||||
/// <summary>
|
||||
///
|
||||
@@ -40,17 +48,41 @@ public static class DocSignedNotificationExtensions
|
||||
/// Converts an <see cref="EnvelopeReceiverDto"/> to a <see cref="DocSignedNotification"/>.
|
||||
/// </summary>
|
||||
/// <param name="dto">The DTO to convert.</param>
|
||||
/// <param name="annotations"></param>
|
||||
/// <param name="psPdfKitAnnotation"></param>
|
||||
/// <returns>A new <see cref="DocSignedNotification"/> instance.</returns>
|
||||
public static DocSignedNotification ToDocSignedNotification(this EnvelopeReceiverDto dto, ExpandoObject annotations)
|
||||
=> new(dto) { Annotations = annotations };
|
||||
public static DocSignedNotification ToDocSignedNotification(this EnvelopeReceiverDto dto, PsPdfKitAnnotation psPdfKitAnnotation)
|
||||
=> new(dto) { PsPdfKitAnnotation = psPdfKitAnnotation };
|
||||
|
||||
/// <summary>
|
||||
/// Asynchronously converts a <see cref="Task{EnvelopeReceiverDto}"/> to a <see cref="DocSignedNotification"/>.
|
||||
///
|
||||
/// </summary>
|
||||
/// <param name="dtoTask">The task that returns the DTO to convert.</param>
|
||||
/// <param name="annotations"></param>
|
||||
/// <returns>A task that represents the asynchronous conversion operation.</returns>
|
||||
public static async Task<DocSignedNotification?> ToDocSignedNotification(this Task<EnvelopeReceiverDto?> dtoTask, ExpandoObject annotations)
|
||||
=> await dtoTask is EnvelopeReceiverDto dto ? new(dto) { Annotations = annotations } : null;
|
||||
/// <param name="dtoTask"></param>
|
||||
/// <param name="psPdfKitAnnotation"></param>
|
||||
/// <returns></returns>
|
||||
public static async Task<DocSignedNotification?> ToDocSignedNotification(this Task<EnvelopeReceiverDto?> dtoTask, PsPdfKitAnnotation psPdfKitAnnotation)
|
||||
=> await dtoTask is EnvelopeReceiverDto dto ? new(dto) { PsPdfKitAnnotation = psPdfKitAnnotation } : null;
|
||||
|
||||
/// <summary>
|
||||
///
|
||||
/// </summary>
|
||||
/// <param name="publisher"></param>
|
||||
/// <param name="notification"></param>
|
||||
/// <param name="cancel"></param>
|
||||
/// <returns></returns>
|
||||
public static async Task PublishSafely(this IPublisher publisher, DocSignedNotification notification, CancellationToken cancel = default)
|
||||
{
|
||||
try
|
||||
{
|
||||
await publisher.Publish(notification, cancel);
|
||||
}
|
||||
catch (Exception)
|
||||
{
|
||||
await publisher.Publish(new RemoveSignatureNotification()
|
||||
{
|
||||
EnvelopeId = notification.EnvelopeId,
|
||||
ReceiverId = notification.ReceiverId
|
||||
}, cancel);
|
||||
throw;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,7 +1,6 @@
|
||||
using EnvelopeGenerator.Application.DocStatus.Commands;
|
||||
using EnvelopeGenerator.Domain.Constants;
|
||||
using DigitalData.Core.Abstraction.Application.Repository;
|
||||
using EnvelopeGenerator.Domain.Entities;
|
||||
using MediatR;
|
||||
using System.Text.Json;
|
||||
|
||||
namespace EnvelopeGenerator.Application.Common.Notifications.DocSigned.Handlers;
|
||||
|
||||
@@ -10,15 +9,18 @@ namespace EnvelopeGenerator.Application.Common.Notifications.DocSigned.Handlers;
|
||||
/// </summary>
|
||||
public class AnnotationHandler : INotificationHandler<DocSignedNotification>
|
||||
{
|
||||
private readonly ISender _sender;
|
||||
/// <summary>
|
||||
///
|
||||
/// </summary>
|
||||
private readonly IRepository<ElementAnnotation> _repo;
|
||||
|
||||
/// <summary>
|
||||
///
|
||||
/// </summary>
|
||||
/// <param name="sender"></param>
|
||||
public AnnotationHandler(ISender sender)
|
||||
/// <param name="repository"></param>
|
||||
public AnnotationHandler(IRepository<ElementAnnotation> repository)
|
||||
{
|
||||
_sender = sender;
|
||||
_repo = repository;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -27,13 +29,6 @@ public class AnnotationHandler : INotificationHandler<DocSignedNotification>
|
||||
/// <param name="notification"></param>
|
||||
/// <param name="cancel"></param>
|
||||
/// <returns></returns>
|
||||
public async Task Handle(DocSignedNotification notification, CancellationToken cancel)
|
||||
{
|
||||
await _sender.Send(new SaveDocStatusCommand()
|
||||
{
|
||||
Envelope = new() { Id = notification.EnvelopeId },
|
||||
Receiver = new() { Id = notification.ReceiverId},
|
||||
Value = JsonSerializer.Serialize(notification.Annotations, Format.Json.ForAnnotations)
|
||||
}, cancel);
|
||||
}
|
||||
public Task Handle(DocSignedNotification notification, CancellationToken cancel)
|
||||
=> _repo.CreateAsync(notification.PsPdfKitAnnotation.Structured, cancel);
|
||||
}
|
||||
@@ -0,0 +1,39 @@
|
||||
using EnvelopeGenerator.Application.DocStatus.Commands;
|
||||
using EnvelopeGenerator.Domain.Constants;
|
||||
using MediatR;
|
||||
using System.Text.Json;
|
||||
|
||||
namespace EnvelopeGenerator.Application.Common.Notifications.DocSigned.Handlers;
|
||||
|
||||
/// <summary>
|
||||
///
|
||||
/// </summary>
|
||||
public class DocStatusHandler : INotificationHandler<DocSignedNotification>
|
||||
{
|
||||
private readonly ISender _sender;
|
||||
|
||||
/// <summary>
|
||||
///
|
||||
/// </summary>
|
||||
/// <param name="sender"></param>
|
||||
public DocStatusHandler(ISender sender)
|
||||
{
|
||||
_sender = sender;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
///
|
||||
/// </summary>
|
||||
/// <param name="notification"></param>
|
||||
/// <param name="cancel"></param>
|
||||
/// <returns></returns>
|
||||
public async Task Handle(DocSignedNotification notification, CancellationToken cancel)
|
||||
{
|
||||
await _sender.Send(new SaveDocStatusCommand()
|
||||
{
|
||||
Envelope = new() { Id = notification.EnvelopeId },
|
||||
Receiver = new() { Id = notification.ReceiverId},
|
||||
Value = JsonSerializer.Serialize(notification.PsPdfKitAnnotation.Instant, Format.Json.ForAnnotations)
|
||||
}, cancel);
|
||||
}
|
||||
}
|
||||
@@ -2,7 +2,6 @@
|
||||
using EnvelopeGenerator.Application.Histories.Commands;
|
||||
using EnvelopeGenerator.Domain.Constants;
|
||||
using MediatR;
|
||||
using Newtonsoft.Json;
|
||||
|
||||
namespace EnvelopeGenerator.Application.Common.Notifications.DocSigned.Handlers;
|
||||
|
||||
|
||||
@@ -0,0 +1,53 @@
|
||||
using DigitalData.Core.Abstraction.Application.Repository;
|
||||
using EnvelopeGenerator.Domain.Entities;
|
||||
using MediatR;
|
||||
|
||||
namespace EnvelopeGenerator.Application.Common.Notifications.RemoveSignature.Handlers;
|
||||
|
||||
/// <summary>
|
||||
///
|
||||
/// </summary>
|
||||
public class RemoveAnnotationHandler : INotificationHandler<RemoveSignatureNotification>
|
||||
{
|
||||
private readonly IRepository<ElementAnnotation> _repo;
|
||||
|
||||
/// <summary>
|
||||
///
|
||||
/// </summary>
|
||||
/// <param name="repository"></param>
|
||||
public RemoveAnnotationHandler(IRepository<ElementAnnotation> repository)
|
||||
{
|
||||
_repo = repository;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
///
|
||||
/// </summary>
|
||||
/// <param name="notification"></param>
|
||||
/// <param name="cancel"></param>
|
||||
/// <returns></returns>
|
||||
public Task Handle(RemoveSignatureNotification notification, CancellationToken cancel)
|
||||
{
|
||||
notification.ThrowIfHasNoFilter();
|
||||
return _repo.DeleteAsync(annots =>
|
||||
{
|
||||
// envelope ID filter
|
||||
if (notification.EnvelopeId is int envelopeId)
|
||||
annots = annots.Where(annot => annot.Element!.Document.EnvelopeId == envelopeId);
|
||||
|
||||
// envelope UUID filter
|
||||
if (notification.EnvelopeUuid is string envelopeUuid)
|
||||
annots = annots.Where(annot => annot.Element!.Document.Envelope!.Uuid == envelopeUuid);
|
||||
|
||||
// receiver ID
|
||||
if (notification.ReceiverId is int receiverId)
|
||||
annots = annots.Where(annot => annot.Element!.ReceiverId == receiverId);
|
||||
|
||||
// receiver signature
|
||||
if (notification.ReceiverSignature is string receiverSignature)
|
||||
annots = annots.Where(annot => annot.Element!.Receiver!.Signature == receiverSignature);
|
||||
|
||||
return annots;
|
||||
}, cancel);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,53 @@
|
||||
using AngleSharp.Html;
|
||||
using DigitalData.Core.Abstraction.Application.Repository;
|
||||
using MediatR;
|
||||
|
||||
namespace EnvelopeGenerator.Application.Common.Notifications.RemoveSignature.Handlers;
|
||||
|
||||
/// <summary>
|
||||
///
|
||||
/// </summary>
|
||||
public class RemoveDocStatusHandler : INotificationHandler<RemoveSignatureNotification>
|
||||
{
|
||||
private readonly IRepository<Domain.Entities.DocumentStatus> _repo;
|
||||
|
||||
/// <summary>
|
||||
///
|
||||
/// </summary>
|
||||
/// <param name="repository"></param>
|
||||
public RemoveDocStatusHandler(IRepository<Domain.Entities.DocumentStatus> repository)
|
||||
{
|
||||
_repo = repository;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
///
|
||||
/// </summary>
|
||||
/// <param name="notification"></param>
|
||||
/// <param name="cancel"></param>
|
||||
/// <returns></returns>
|
||||
public Task Handle(RemoveSignatureNotification notification, CancellationToken cancel)
|
||||
{
|
||||
notification.ThrowIfHasNoFilter();
|
||||
return _repo.DeleteAsync(statuses =>
|
||||
{
|
||||
// envelope ID filter
|
||||
if (notification.EnvelopeId is int envelopeId)
|
||||
statuses = statuses.Where(status => status.EnvelopeId == envelopeId);
|
||||
|
||||
// envelope UUID filter
|
||||
if (notification.EnvelopeUuid is string envelopeUuid)
|
||||
statuses = statuses.Where(status => status.Envelope!.Uuid == envelopeUuid);
|
||||
|
||||
// receiver Id filter
|
||||
if (notification.ReceiverId is int receiverId)
|
||||
statuses = statuses.Where(status => status.ReceiverId == receiverId);
|
||||
|
||||
// receiver signature filter
|
||||
if (notification.ReceiverSignature is string receiverSignature)
|
||||
statuses = statuses.Where(status => status.Receiver!.Signature == receiverSignature);
|
||||
|
||||
return statuses;
|
||||
}, cancel);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,56 @@
|
||||
using DigitalData.Core.Abstraction.Application.Repository;
|
||||
using EnvelopeGenerator.Application.Common.Extensions;
|
||||
using EnvelopeGenerator.Domain.Constants;
|
||||
using MediatR;
|
||||
|
||||
namespace EnvelopeGenerator.Application.Common.Notifications.RemoveSignature.Handlers;
|
||||
|
||||
/// <summary>
|
||||
///
|
||||
/// </summary>
|
||||
public class RemoveHistoryHandler : INotificationHandler<RemoveSignatureNotification>
|
||||
{
|
||||
private readonly IRepository<Domain.Entities.History> _repo;
|
||||
|
||||
/// <summary>
|
||||
///
|
||||
/// </summary>
|
||||
/// <param name="repository"></param>
|
||||
public RemoveHistoryHandler(IRepository<Domain.Entities.History> repository)
|
||||
{
|
||||
_repo = repository;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
///
|
||||
/// </summary>
|
||||
/// <param name="notification"></param>
|
||||
/// <param name="cancel"></param>
|
||||
/// <returns></returns>
|
||||
public Task Handle(RemoveSignatureNotification notification, CancellationToken cancel)
|
||||
{
|
||||
notification.ThrowIfHasNoFilter();
|
||||
return _repo.DeleteAsync(hists =>
|
||||
{
|
||||
hists = hists.Where(hist => hist.Status == EnvelopeStatus.DocumentSigned);
|
||||
|
||||
// envelope ID filter
|
||||
if (notification.EnvelopeId is int envelopeId)
|
||||
hists = hists.Where(hist => hist.EnvelopeId == envelopeId);
|
||||
|
||||
// envelope UUID filter
|
||||
if (notification.EnvelopeUuid is string envelopeUuid)
|
||||
hists = hists.Where(hist => hist.Envelope!.Uuid == envelopeUuid);
|
||||
|
||||
// receiver ID filter
|
||||
if (notification.ReceiverId is int receiverId)
|
||||
hists = hists.Where(hist => hist.Receiver!.Id == receiverId);
|
||||
|
||||
// receiver signature filter
|
||||
if (notification.ReceiverSignature is string receiverSignature)
|
||||
hists = hists.Where(hist => hist.Receiver!.Signature == receiverSignature);
|
||||
|
||||
return hists;
|
||||
}, cancel);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,37 @@
|
||||
using MediatR;
|
||||
|
||||
namespace EnvelopeGenerator.Application.Common.Notifications.RemoveSignature;
|
||||
|
||||
/// <summary>
|
||||
///
|
||||
/// </summary>
|
||||
/// <param name="EnvelopeId"></param>
|
||||
/// <param name="ReceiverId"></param>
|
||||
/// <param name="EnvelopeUuid"></param>
|
||||
/// <param name="ReceiverSignature"></param>
|
||||
public record RemoveSignatureNotification(
|
||||
int? EnvelopeId = null,
|
||||
int? ReceiverId = null,
|
||||
string? EnvelopeUuid = null,
|
||||
string? ReceiverSignature = null
|
||||
) : INotification
|
||||
{
|
||||
/// <summary>
|
||||
///
|
||||
/// </summary>
|
||||
public bool HasFilter =>
|
||||
EnvelopeId is not null
|
||||
|| ReceiverId is not null
|
||||
|| EnvelopeUuid is not null
|
||||
|| ReceiverSignature is not null;
|
||||
|
||||
/// <summary>
|
||||
///
|
||||
/// </summary>
|
||||
/// <exception cref="InvalidOperationException"></exception>
|
||||
public void ThrowIfHasNoFilter()
|
||||
{
|
||||
if (!HasFilter)
|
||||
throw new InvalidOperationException("At least one filter parameter must be provided.");
|
||||
}
|
||||
}
|
||||
@@ -14,7 +14,7 @@
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Dapper" Version="2.1.66" />
|
||||
<PackageReference Include="DigitalData.Core.Abstraction.Application" Version="1.3.5" />
|
||||
<PackageReference Include="DigitalData.Core.Abstraction.Application" Version="1.4.0" />
|
||||
<PackageReference Include="DigitalData.Core.Application" Version="3.4.0" />
|
||||
<PackageReference Include="DigitalData.Core.Client" Version="2.1.0" />
|
||||
<PackageReference Include="DigitalData.Core.Exceptions" Version="1.1.0" />
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -70,11 +70,11 @@
|
||||
<Reference Include="DigitalData.Controls.DocumentViewer, Version=1.9.8.0, Culture=neutral, processorArchitecture=MSIL">
|
||||
<HintPath>..\packages\DigitalData.Controls.DocumentViewer.1.9.8\lib\net462\DigitalData.Controls.DocumentViewer.dll</HintPath>
|
||||
</Reference>
|
||||
<Reference Include="DigitalData.Core.Abstraction.Application, Version=1.3.5.0, Culture=neutral, processorArchitecture=MSIL">
|
||||
<HintPath>..\packages\DigitalData.Core.Abstraction.Application.1.3.5\lib\net462\DigitalData.Core.Abstraction.Application.dll</HintPath>
|
||||
<Reference Include="DigitalData.Core.Abstraction.Application, Version=1.4.0.0, Culture=neutral, processorArchitecture=MSIL">
|
||||
<HintPath>..\packages\DigitalData.Core.Abstraction.Application.1.4.0\lib\net462\DigitalData.Core.Abstraction.Application.dll</HintPath>
|
||||
</Reference>
|
||||
<Reference Include="DigitalData.Core.Abstractions, Version=4.1.1.0, Culture=neutral, processorArchitecture=MSIL">
|
||||
<HintPath>..\packages\DigitalData.Core.Abstractions.4.1.1\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>
|
||||
|
||||
@@ -5,9 +5,10 @@ Imports EnvelopeGenerator.CommonServices.Jobs
|
||||
Imports EnvelopeGenerator.CommonServices.Jobs.FinalizeDocument
|
||||
Imports GdPicture14
|
||||
Imports Newtonsoft.Json.Linq
|
||||
Imports DigitalData.Core.Abstraction.Application
|
||||
Imports EnvelopeGenerator.Infrastructure
|
||||
Imports Microsoft.EntityFrameworkCore
|
||||
Imports System.Text
|
||||
Imports DigitalData.Core.Abstractions
|
||||
|
||||
Public Class frmFinalizePDF
|
||||
Private Const CONNECTIONSTRING = "Server=sDD-VMP04-SQL17\DD_DEVELOP01;Database=DD_ECM;User Id=sa;Password=+bk8oAbbQP1AzoHtvZUbd+Mbok2f8Fl4miEx1qssJ5yEaEWoQJ9prg4L14fURpPnqi1WMNs9fE4=;"
|
||||
@@ -23,10 +24,15 @@ Public Class frmFinalizePDF
|
||||
|
||||
Private Sub frmFinalizePDF_Load(sender As Object, e As EventArgs) Handles MyBase.Load
|
||||
LogConfig = New LogConfig(LogConfig.PathType.CustomPath, Application.StartupPath)
|
||||
Database = New MSSQLServer(LogConfig, MSSQLServer.DecryptConnectionString(CONNECTIONSTRING))
|
||||
|
||||
Dim dCnnStr As String = MSSQLServer.DecryptConnectionString(CONNECTIONSTRING)
|
||||
|
||||
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)
|
||||
@@ -39,7 +45,7 @@ Public Class frmFinalizePDF
|
||||
End Sub)
|
||||
opt.AddDbContext(
|
||||
Sub(options)
|
||||
options.UseSqlServer(CONNECTIONSTRING) _
|
||||
options.UseSqlServer(dCnnStr) _
|
||||
.EnableSensitiveDataLogging() _
|
||||
.EnableDetailedErrors()
|
||||
End Sub)
|
||||
@@ -95,8 +101,9 @@ Public Class frmFinalizePDF
|
||||
Select(Function(r As DataRow) r.Item("VALUE").ToString()).
|
||||
ToList()
|
||||
|
||||
Dim oBuffer As Byte() = ReadEnvelope(CInt(txtEnvelope.Text))
|
||||
Dim oNewBuffer = PDFBurner.BurnInstantJSONAnnotationsToPDF(oBuffer, oJsonList)
|
||||
Dim envelopeId As Integer = CInt(txtEnvelope.Text)
|
||||
Dim oBuffer As Byte() = ReadEnvelope(envelopeId)
|
||||
Dim oNewBuffer = PDFBurner.BurnAnnotsToPDF(oBuffer, oJsonList, envelopeId)
|
||||
Dim desktopPath As String = Environment.GetFolderPath(Environment.SpecialFolder.Desktop)
|
||||
Dim oNewPath = Path.Combine(desktopPath, $"E{txtEnvelope.Text}R{txtReceiver.Text}.burned.pdf")
|
||||
|
||||
@@ -104,7 +111,15 @@ Public Class frmFinalizePDF
|
||||
|
||||
Process.Start(oNewPath)
|
||||
Catch ex As Exception
|
||||
MsgBox(ex.Message, MsgBoxStyle.Critical)
|
||||
Dim exMsg As StringBuilder = New StringBuilder(ex.Message).AppendLine()
|
||||
|
||||
Dim innerEx = ex.InnerException
|
||||
While (innerEx IsNot Nothing)
|
||||
exMsg.AppendLine(innerEx.Message)
|
||||
innerEx = innerEx.InnerException
|
||||
End While
|
||||
|
||||
MsgBox(exMsg.ToString(), MsgBoxStyle.Critical)
|
||||
End Try
|
||||
|
||||
End Sub
|
||||
|
||||
@@ -3,8 +3,8 @@
|
||||
<package id="AutoMapper" version="10.1.1" targetFramework="net462" />
|
||||
<package id="BouncyCastle.Cryptography" version="2.5.0" targetFramework="net462" />
|
||||
<package id="DigitalData.Controls.DocumentViewer" version="1.9.8" targetFramework="net462" />
|
||||
<package id="DigitalData.Core.Abstraction.Application" version="1.3.5" targetFramework="net462" />
|
||||
<package id="DigitalData.Core.Abstractions" version="4.1.1" targetFramework="net462" />
|
||||
<package id="DigitalData.Core.Abstraction.Application" version="1.4.0" targetFramework="net462" />
|
||||
<package id="DigitalData.Core.Abstractions" version="4.3.0" targetFramework="net462" />
|
||||
<package id="DigitalData.Modules.Base" version="1.3.8" targetFramework="net462" />
|
||||
<package id="DigitalData.Modules.Config" version="1.3.0" targetFramework="net462" />
|
||||
<package id="DigitalData.Modules.Database" version="2.3.5.4" targetFramework="net462" />
|
||||
|
||||
@@ -72,11 +72,12 @@
|
||||
<Reference Include="DevExpress.XtraEditors.v21.2, Version=21.2.4.0, Culture=neutral, PublicKeyToken=b88d1754d700e49a" />
|
||||
<Reference Include="DevExpress.XtraGauges.v21.2.Core, Version=21.2.4.0, Culture=neutral, PublicKeyToken=b88d1754d700e49a, processorArchitecture=MSIL" />
|
||||
<Reference Include="DevExpress.XtraReports.v21.2, Version=21.2.4.0, Culture=neutral, PublicKeyToken=b88d1754d700e49a, processorArchitecture=MSIL" />
|
||||
<Reference Include="DigitalData.Core.Abstraction.Application, Version=1.3.5.0, Culture=neutral, processorArchitecture=MSIL">
|
||||
<HintPath>..\packages\DigitalData.Core.Abstraction.Application.1.3.5\lib\net462\DigitalData.Core.Abstraction.Application.dll</HintPath>
|
||||
<Reference Include="DigitalData.Core.Abstraction.Application, Version=1.4.0.0, Culture=neutral, processorArchitecture=MSIL">
|
||||
<HintPath>..\packages\DigitalData.Core.Abstraction.Application.1.4.0\lib\net462\DigitalData.Core.Abstraction.Application.dll</HintPath>
|
||||
</Reference>
|
||||
<Reference Include="DigitalData.Core.Abstractions, Version=4.1.1.0, Culture=neutral, processorArchitecture=MSIL">
|
||||
<HintPath>..\packages\DigitalData.Core.Abstractions.4.1.1\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>
|
||||
|
||||
@@ -14,6 +14,7 @@ Imports EnvelopeGenerator.Domain.Entities
|
||||
Imports DigitalData.Core.Abstraction.Application
|
||||
Imports EnvelopeGenerator.Infrastructure
|
||||
Imports Microsoft.EntityFrameworkCore
|
||||
Imports DigitalData.Core.Abstractions
|
||||
|
||||
Namespace Jobs
|
||||
Public Class FinalizeDocumentJob
|
||||
@@ -69,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)
|
||||
@@ -403,7 +406,6 @@ Namespace Jobs
|
||||
ParentFolderUID = pEnvelopeData.EnvelopeUUID
|
||||
End If
|
||||
|
||||
|
||||
Logger.Info("ParentFolderUID: [{0}]", ParentFolderUID)
|
||||
Dim oInputDocumentBuffer As Byte()
|
||||
If Not IsNothing(pEnvelopeData.DocAsByte) Then
|
||||
@@ -416,7 +418,7 @@ Namespace Jobs
|
||||
End Try
|
||||
End If
|
||||
|
||||
Return PDFBurner.BurnInstantJSONAnnotationsToPDF(oInputDocumentBuffer, oAnnotations)
|
||||
Return PDFBurner.BurnAnnotsToPDF(oInputDocumentBuffer, oAnnotations, pEnvelopeData.EnvelopeId)
|
||||
End Function
|
||||
|
||||
Private Function GetEnvelopeData(pEnvelopeId As Integer) As EnvelopeData
|
||||
|
||||
@@ -1,11 +1,17 @@
|
||||
Imports System.Drawing
|
||||
Imports System.Collections.Immutable
|
||||
Imports System.Drawing
|
||||
Imports System.IO
|
||||
Imports DevExpress.DataProcessing
|
||||
Imports DigitalData.Core.Abstraction.Application.Repository
|
||||
Imports DigitalData.Core.Abstractions
|
||||
Imports DigitalData.Modules.Base
|
||||
Imports DigitalData.Modules.Logging
|
||||
Imports GdPicture14
|
||||
Imports Newtonsoft.Json
|
||||
Imports EnvelopeGenerator.CommonServices.Jobs.FinalizeDocument.FinalizeDocumentExceptions
|
||||
Imports DevExpress.DataProcessing
|
||||
Imports EnvelopeGenerator.Domain.Entities
|
||||
Imports EnvelopeGenerator.PdfEditor
|
||||
Imports GdPicture14
|
||||
Imports Microsoft.EntityFrameworkCore
|
||||
Imports Newtonsoft.Json
|
||||
|
||||
Namespace Jobs.FinalizeDocument
|
||||
Public Class PDFBurner
|
||||
@@ -14,9 +20,6 @@ Namespace Jobs.FinalizeDocument
|
||||
Private ReadOnly Manager As AnnotationManager
|
||||
Private ReadOnly LicenseManager As LicenseManager
|
||||
|
||||
Private Const ANNOTATION_TYPE_IMAGE = "pspdfkit/image"
|
||||
Private Const ANNOTATION_TYPE_INK = "pspdfkit/ink"
|
||||
Private Const ANNOTATION_TYPE_WIDGET = "pspdfkit/widget"
|
||||
Private Property _pdfBurnerParams As PDFBurnerParams
|
||||
|
||||
Public Sub New(pLogConfig As LogConfig, pGDPictureLicenseKey As String, pdfBurnerParams As PDFBurnerParams)
|
||||
@@ -30,7 +33,94 @@ Namespace Jobs.FinalizeDocument
|
||||
_pdfBurnerParams = pdfBurnerParams
|
||||
End Sub
|
||||
|
||||
Public Function BurnInstantJSONAnnotationsToPDF(pSourceBuffer As Byte(), pInstantJSONList As List(Of String)) As Byte()
|
||||
#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
|
||||
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(),
|
||||
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)
|
||||
'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
|
||||
|
||||
Public Function BurnInstantJSONAnnotsToPDF(pSourceBuffer As Byte(), pInstantJSONList As List(Of String)) As Byte()
|
||||
Dim oResult As GdPictureStatus
|
||||
Using oSourceStream As New MemoryStream(pSourceBuffer)
|
||||
' Open PDF
|
||||
@@ -67,47 +157,42 @@ Namespace Jobs.FinalizeDocument
|
||||
End Using
|
||||
End Using
|
||||
End Function
|
||||
#End Region
|
||||
|
||||
#Region "Add Value"
|
||||
Private Sub AddInstantJSONAnnotationToPDF(pInstantJSON As String)
|
||||
Dim oAnnotationData = JsonConvert.DeserializeObject(Of AnnotationData)(pInstantJSON)
|
||||
|
||||
oAnnotationData.annotations.Reverse()
|
||||
|
||||
Dim yPosOfSigAnnot = oAnnotationData.annotations.ElementAt(2).bbox.ElementAt(1) - 71.84002685546875 + 7
|
||||
Dim isSeal = True 'First element is signature seal
|
||||
For Each oAnnotation In oAnnotationData.annotations
|
||||
Logger.Debug("Adding AnnotationID: " + oAnnotation.id)
|
||||
|
||||
Dim formFieldIndex = 0
|
||||
For Each oAnnotation In oAnnotationData.annotations
|
||||
Logger.Debug("Adding AnnotationID: " + oAnnotation.id)
|
||||
Select Case oAnnotation.type
|
||||
Case AnnotationType.Image
|
||||
AddImageAnnotation(oAnnotation, oAnnotationData.attachments)
|
||||
Exit Select
|
||||
Case AnnotationType.Ink
|
||||
AddInkAnnotation(oAnnotation)
|
||||
Exit Select
|
||||
Case AnnotationType.Widget
|
||||
'Add form field values
|
||||
Dim formFieldValue = oAnnotationData.formFieldValues.FirstOrDefault(Function(fv) fv.name = oAnnotation.id)
|
||||
If formFieldValue IsNot Nothing AndAlso Not _pdfBurnerParams.IgnoredLabels.Contains(formFieldValue.value) Then
|
||||
AddFormFieldValue(oAnnotation, formFieldValue)
|
||||
End If
|
||||
Exit Select
|
||||
End Select
|
||||
Next
|
||||
|
||||
Select Case oAnnotation.type
|
||||
Case ANNOTATION_TYPE_IMAGE
|
||||
|
||||
If (isSeal) Then
|
||||
oAnnotation.bbox.Item(1) = yPosOfSigAnnot
|
||||
End If
|
||||
|
||||
AddImageAnnotation(oAnnotation, oAnnotationData.attachments)
|
||||
Exit Select
|
||||
|
||||
Case ANNOTATION_TYPE_INK
|
||||
AddInkAnnotation(oAnnotation)
|
||||
Exit Select
|
||||
|
||||
Case ANNOTATION_TYPE_WIDGET
|
||||
'Add form field values
|
||||
Dim formFieldValue = oAnnotationData.formFieldValues.FirstOrDefault(Function(fv) fv.name = oAnnotation.id)
|
||||
If formFieldValue IsNot Nothing AndAlso Not _pdfBurnerParams.IgnoredLabels.Contains(formFieldValue.value) Then
|
||||
AddFormFieldValue(oAnnotation, formFieldValue, formFieldIndex)
|
||||
formFieldIndex += 1
|
||||
End If
|
||||
Exit Select
|
||||
End Select
|
||||
|
||||
isSeal = False
|
||||
Next
|
||||
End Sub
|
||||
|
||||
Private Function AddImageAnnotation(pAnnotation As Annotation, pAttachments As Dictionary(Of String, Attachment)) As Void
|
||||
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()
|
||||
|
||||
@@ -121,9 +206,26 @@ Namespace Jobs.FinalizeDocument
|
||||
|
||||
Manager.SelectPage(pAnnotation.pageIndex + 1)
|
||||
Manager.AddEmbeddedImageAnnotFromBase64(oAttachment.Value.binary, oX, oY, oWidth, oHeight)
|
||||
End Function
|
||||
End Sub
|
||||
|
||||
Private Function AddInkAnnotation(pAnnotation As Annotation) As Void
|
||||
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)
|
||||
Manager.SelectPage(pAnnotation.pageIndex + 1)
|
||||
@@ -135,14 +237,29 @@ Namespace Jobs.FinalizeDocument
|
||||
|
||||
Manager.AddFreeHandAnnot(oColor, oPoints)
|
||||
Next
|
||||
End Function
|
||||
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)
|
||||
|
||||
Private Function AddFormFieldValue(pAnnotation As Annotation, formFieldValue As FormFieldValue, index As Integer) As Void
|
||||
' Convert pixels to Inches
|
||||
Dim oBounds = pAnnotation.bbox.Select(AddressOf ToInches).ToList()
|
||||
|
||||
Dim oX = oBounds.Item(0)
|
||||
Dim oY = oBounds.Item(1) + _pdfBurnerParams.YOffset * index + _pdfBurnerParams.TopMargin
|
||||
Dim oY = oBounds.Item(1) + _pdfBurnerParams.YOffset * ffIndex + _pdfBurnerParams.TopMargin
|
||||
Dim oWidth = oBounds.Item(2)
|
||||
Dim oHeight = oBounds.Item(3)
|
||||
|
||||
@@ -155,8 +272,10 @@ Namespace Jobs.FinalizeDocument
|
||||
ant.FontSize = _pdfBurnerParams.FontSize
|
||||
ant.FontStyle = _pdfBurnerParams.FontStyle
|
||||
Manager.SaveAnnotationsToPage()
|
||||
End Function
|
||||
End Sub
|
||||
#End Region
|
||||
|
||||
#Region "Helpers"
|
||||
Private Function ToPointF(pPoints As List(Of Single)) As PointF
|
||||
Dim oPoints = pPoints.Select(AddressOf ToInches).ToList()
|
||||
Return New PointF(oPoints.Item(0), oPoints.Item(1))
|
||||
@@ -169,22 +288,34 @@ Namespace Jobs.FinalizeDocument
|
||||
Private Function ToInches(pValue As Single) As Single
|
||||
Return pValue / 72
|
||||
End Function
|
||||
#End Region
|
||||
|
||||
#Region "Model"
|
||||
Friend Class AnnotationType
|
||||
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
|
||||
Public Property annotations As List(Of Annotation)
|
||||
|
||||
Public ReadOnly Property SignatureAnnotations As List(Of List(Of Annotation))
|
||||
Public ReadOnly Property AnnotationsByReceiver As IEnumerable(Of List(Of Annotation))
|
||||
Get
|
||||
Dim result As New List(Of List(Of Annotation))()
|
||||
Return annotations _
|
||||
.Where(Function(annot) annot.hasStructuredID).ToList() _
|
||||
.GroupBy(Function(a) a.receiverId) _
|
||||
.Select(Function(g) g.ToList())
|
||||
End Get
|
||||
End Property
|
||||
|
||||
If annotations IsNot Nothing AndAlso annotations.Count > 0 Then
|
||||
For i As Integer = 0 To annotations.Count - 1 Step 6
|
||||
Dim group As List(Of Annotation) = annotations.Skip(i).Take(6).ToList()
|
||||
result.Add(group)
|
||||
Next
|
||||
End If
|
||||
|
||||
Return result
|
||||
Public ReadOnly Property UnstructuredAnnotations As IEnumerable(Of List(Of Annotation))
|
||||
Get
|
||||
Return annotations _
|
||||
.Where(Function(annot) Not annot.hasStructuredID).ToList() _
|
||||
.GroupBy(Function(a) a.receiverId) _
|
||||
.Select(Function(g) g.ToList())
|
||||
End Get
|
||||
End Property
|
||||
|
||||
@@ -193,16 +324,102 @@ Namespace Jobs.FinalizeDocument
|
||||
End Class
|
||||
|
||||
Friend Class Annotation
|
||||
|
||||
Private _id As String = Nothing
|
||||
|
||||
Public envelopeId As Integer = Nothing
|
||||
|
||||
Public receiverId As Integer = Nothing
|
||||
|
||||
Public index As Integer = Nothing
|
||||
|
||||
Public egName As String = PDFBurner.EGName.NoName
|
||||
|
||||
Public hasStructuredID As Boolean = False
|
||||
|
||||
Public ReadOnly Property isLabel As Boolean
|
||||
Get
|
||||
If String.IsNullOrEmpty(egName) Then
|
||||
Return False
|
||||
End If
|
||||
|
||||
Dim parts As String() = egName.Split("_"c)
|
||||
Return parts.Length > 1 AndAlso parts(1) = "label"
|
||||
End Get
|
||||
End Property
|
||||
|
||||
Public Property id As String
|
||||
Get
|
||||
Return _id
|
||||
End Get
|
||||
Set(value As String)
|
||||
_id = value
|
||||
|
||||
If String.IsNullOrWhiteSpace(_id) Then
|
||||
Throw New BurnAnnotationException("The identifier of annotation is null or empty.")
|
||||
End If
|
||||
|
||||
Dim parts As String() = value.Split("#"c)
|
||||
|
||||
If (parts.Length <> 4) Then
|
||||
Return
|
||||
'Throw New BurnAnnotationException($"The identifier of annotation has more or less than 4 sub-part. Id: {_id}")
|
||||
End If
|
||||
|
||||
If Not Integer.TryParse(parts(0), envelopeId) Then
|
||||
Throw New BurnAnnotationException($"The envelope ID of annotation is not integer. Id: {_id}")
|
||||
End If
|
||||
|
||||
If Not Integer.TryParse(parts(1), receiverId) Then
|
||||
Throw New BurnAnnotationException($"The receiver ID of annotation is not integer. Id: {_id}")
|
||||
End If
|
||||
|
||||
If Not Integer.TryParse(parts(2), index) Then
|
||||
Throw New BurnAnnotationException($"The index of annotation is not integer. Id: {_id}")
|
||||
End If
|
||||
|
||||
egName = parts(3)
|
||||
|
||||
hasStructuredID = True
|
||||
End Set
|
||||
End Property
|
||||
|
||||
Public Property bbox As List(Of Double)
|
||||
|
||||
Public Property type As String
|
||||
|
||||
Public Property isSignature As Boolean
|
||||
|
||||
Public Property imageAttachmentId As String
|
||||
|
||||
Public Property lines As Lines
|
||||
|
||||
Public Property pageIndex As Integer
|
||||
|
||||
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()
|
||||
|
||||
Public Shared ReadOnly Seal As String = "signature"
|
||||
|
||||
Public Shared ReadOnly Index As ImmutableDictionary(Of String, Integer) =
|
||||
New Dictionary(Of String, Integer) From {
|
||||
{NoName, 0},
|
||||
{Seal, 0},
|
||||
{"position", 1},
|
||||
{"city", 2},
|
||||
{"date", 3}
|
||||
}.ToImmutableDictionary()
|
||||
End Class
|
||||
|
||||
Friend Class Lines
|
||||
Public Property points As List(Of List(Of List(Of Single)))
|
||||
End Class
|
||||
@@ -216,5 +433,6 @@ Namespace Jobs.FinalizeDocument
|
||||
Public Property name As String
|
||||
Public Property value As String
|
||||
End Class
|
||||
#End Region
|
||||
End Class
|
||||
End Namespace
|
||||
End Namespace
|
||||
@@ -2,8 +2,8 @@
|
||||
<packages>
|
||||
<package id="AutoMapper" version="10.1.1" targetFramework="net462" />
|
||||
<package id="BouncyCastle.Cryptography" version="2.5.0" targetFramework="net462" />
|
||||
<package id="DigitalData.Core.Abstraction.Application" version="1.3.5" targetFramework="net462" />
|
||||
<package id="DigitalData.Core.Abstractions" version="4.1.1" targetFramework="net462" />
|
||||
<package id="DigitalData.Core.Abstraction.Application" version="1.4.0" targetFramework="net462" />
|
||||
<package id="DigitalData.Core.Abstractions" version="4.3.0" targetFramework="net462" />
|
||||
<package id="DigitalData.Modules.Base" version="1.3.8" targetFramework="net462" />
|
||||
<package id="DigitalData.Modules.Config" version="1.3.0" targetFramework="net462" />
|
||||
<package id="DigitalData.Modules.Database" version="2.3.5.4" targetFramework="net462" />
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
using System.ComponentModel.DataAnnotations;
|
||||
using EnvelopeGenerator.Domain.Interfaces;
|
||||
using System.ComponentModel.DataAnnotations;
|
||||
using System.ComponentModel.DataAnnotations.Schema;
|
||||
#if NETFRAMEWORK
|
||||
using System.Drawing;
|
||||
@@ -14,7 +15,7 @@ namespace EnvelopeGenerator.Domain.Entities
|
||||
#endif
|
||||
|
||||
[Table("TBSIG_ENVELOPE_DOCUMENT", Schema = "dbo")]
|
||||
public class Document
|
||||
public class Document : IHasEnvelope
|
||||
{
|
||||
public Document()
|
||||
{
|
||||
@@ -30,27 +31,50 @@ public class Document
|
||||
|
||||
[Required]
|
||||
[Column("ENVELOPE_ID")]
|
||||
public int EnvelopeId { get; set; } = 0;
|
||||
public int EnvelopeId { get; set; }
|
||||
#if NETFRAMEWORK
|
||||
= 0;
|
||||
#endif
|
||||
|
||||
[Column("BYTE_DATA", TypeName = "varbinary(max)")]
|
||||
public byte[] ByteData { get; set; }
|
||||
public byte[]
|
||||
#if NET
|
||||
?
|
||||
#endif
|
||||
ByteData { get; set; }
|
||||
|
||||
public List<Signature> Elements { get; set; }
|
||||
#region File
|
||||
[Column("FILENAME", TypeName = "nvarchar(256)")]
|
||||
public string Filename { get; set; }
|
||||
|
||||
// TODO: * Check the Form App and remove the default value
|
||||
[NotMapped]
|
||||
[Column("FILEPATH", TypeName = "nvarchar(256)")]
|
||||
public string Filepath { get; set; }
|
||||
|
||||
[Column("FILENAME_ORIGINAL", TypeName = "nvarchar(256)")]
|
||||
public string
|
||||
#if NET
|
||||
?
|
||||
#endif
|
||||
FileNameOriginal { get; set; }
|
||||
#endregion
|
||||
|
||||
public virtual List<Signature>
|
||||
#if NET
|
||||
?
|
||||
#endif
|
||||
Elements { get; set; }
|
||||
|
||||
[ForeignKey("EnvelopeId")]
|
||||
public virtual Envelope
|
||||
#if NET
|
||||
?
|
||||
#endif
|
||||
Envelope { get; set; }
|
||||
|
||||
#if NETFRAMEWORK
|
||||
[NotMapped]
|
||||
public string FileNameOriginal { get; set; }
|
||||
|
||||
[NotMapped]
|
||||
public bool IsTempFile { get; set; }
|
||||
|
||||
[NotMapped]
|
||||
public string Filename { get; set; }
|
||||
|
||||
[NotMapped]
|
||||
public Bitmap Thumbnail { get; set; }
|
||||
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
using System.ComponentModel.DataAnnotations;
|
||||
using System.ComponentModel.DataAnnotations.Schema;
|
||||
using DigitalData.Core.Abstractions.Interfaces;
|
||||
using EnvelopeGenerator.Domain.Interfaces;
|
||||
|
||||
#if NETFRAMEWORK
|
||||
@@ -14,7 +15,7 @@ namespace EnvelopeGenerator.Domain.Entities
|
||||
#endif
|
||||
|
||||
[Table("TBSIG_DOCUMENT_STATUS", Schema = "dbo")]
|
||||
public class DocumentStatus : IHasEnvelope, IHasReceiver
|
||||
public class DocumentStatus : IHasEnvelope, IHasReceiver, IEntity
|
||||
{
|
||||
public DocumentStatus()
|
||||
{
|
||||
|
||||
92
EnvelopeGenerator.Domain/Entities/ElementAnnotation.cs
Normal file
92
EnvelopeGenerator.Domain/Entities/ElementAnnotation.cs
Normal file
@@ -0,0 +1,92 @@
|
||||
using System.ComponentModel.DataAnnotations;
|
||||
using System.ComponentModel.DataAnnotations.Schema;
|
||||
#if NETFRAMEWORK
|
||||
using System;
|
||||
#endif
|
||||
|
||||
namespace EnvelopeGenerator.Domain.Entities
|
||||
#if NET
|
||||
;
|
||||
#elif NETFRAMEWORK
|
||||
{
|
||||
#endif
|
||||
|
||||
[Table("TBSIG_DOCUMENT_RECEIVER_ELEMENT_ANNOTATION")]
|
||||
public class ElementAnnotation
|
||||
{
|
||||
[Key]
|
||||
[DatabaseGenerated(DatabaseGeneratedOption.Identity)]
|
||||
[Column("GUID", TypeName = "bigint")]
|
||||
public long Id { get; set; }
|
||||
|
||||
[Required]
|
||||
[Column("ELEMENT_ID", TypeName = "int")]
|
||||
public int ElementId { get; set; }
|
||||
|
||||
[Required]
|
||||
[Column("NAME", TypeName = "nvarchar(100)")]
|
||||
[StringLength(100)]
|
||||
public string Name { get; set; }
|
||||
|
||||
[Required]
|
||||
[Column("VALUE", TypeName = "nvarchar(max)")]
|
||||
public string Value { get; set; }
|
||||
|
||||
[Required]
|
||||
[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; }
|
||||
|
||||
[Column("CHANGED_WHEN", TypeName = "datetime")]
|
||||
public DateTime? ChangedWhen { get; set; }
|
||||
|
||||
[Column("CHANGED_WHO", TypeName = "nvarchar(100)")]
|
||||
[StringLength(100)]
|
||||
public string
|
||||
#if NET
|
||||
?
|
||||
#endif
|
||||
ChangedWho { get; set; }
|
||||
|
||||
[ForeignKey("ElementId")]
|
||||
public virtual Signature
|
||||
#if NET
|
||||
?
|
||||
#endif
|
||||
Element { get; set; }
|
||||
}
|
||||
|
||||
#if NETFRAMEWORK
|
||||
}
|
||||
#endif
|
||||
@@ -3,6 +3,7 @@ using System.ComponentModel.DataAnnotations;
|
||||
using System.ComponentModel.DataAnnotations.Schema;
|
||||
using EnvelopeGenerator.Domain.Interfaces;
|
||||
using EnvelopeGenerator.Domain.Constants;
|
||||
using DigitalData.Core.Abstractions.Interfaces;
|
||||
|
||||
|
||||
#if NETFRAMEWORK
|
||||
@@ -17,7 +18,7 @@ namespace EnvelopeGenerator.Domain.Entities
|
||||
#endif
|
||||
|
||||
[Table("TBSIG_ENVELOPE_HISTORY", Schema = "dbo")]
|
||||
public class History : IHasEnvelope, IHasReceiver
|
||||
public class History : IHasEnvelope, IHasReceiver, IEntity
|
||||
{
|
||||
[Key]
|
||||
[DatabaseGenerated(DatabaseGeneratedOption.Identity)]
|
||||
|
||||
@@ -4,6 +4,7 @@ using System.ComponentModel.DataAnnotations;
|
||||
using System.ComponentModel.DataAnnotations.Schema;
|
||||
#if NETFRAMEWORK
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
#endif
|
||||
|
||||
namespace EnvelopeGenerator.Domain.Entities
|
||||
@@ -14,7 +15,7 @@ namespace EnvelopeGenerator.Domain.Entities
|
||||
#endif
|
||||
|
||||
[Table("TBSIG_DOCUMENT_RECEIVER_ELEMENT", Schema = "dbo")]
|
||||
public class Signature : ISignature
|
||||
public class Signature : ISignature, IHasReceiver
|
||||
{
|
||||
public Signature()
|
||||
{
|
||||
@@ -107,7 +108,17 @@ public class Signature : ISignature
|
||||
public virtual Document Document { get; set; }
|
||||
|
||||
[ForeignKey("ReceiverId")]
|
||||
public virtual Receiver Receiver { get; set; }
|
||||
public virtual Receiver
|
||||
#if NET
|
||||
?
|
||||
#endif
|
||||
Receiver { get; set; }
|
||||
|
||||
public virtual IEnumerable<ElementAnnotation>
|
||||
#if NET
|
||||
?
|
||||
#endif
|
||||
Annotations { get; set; }
|
||||
|
||||
#if NETFRAMEWORK
|
||||
[NotMapped]
|
||||
|
||||
@@ -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.1.1" />
|
||||
<PackageReference Include="DigitalData.Core.Abstractions" Version="4.3.0" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
|
||||
@@ -201,7 +201,7 @@
|
||||
</dependentAssembly>
|
||||
<dependentAssembly>
|
||||
<assemblyIdentity name="Microsoft.Identity.Client" publicKeyToken="0a613f4dd989e8ae" culture="neutral" />
|
||||
<bindingRedirect oldVersion="0.0.0.0-4.55.0.0" newVersion="4.55.0.0" />
|
||||
<bindingRedirect oldVersion="0.0.0.0-3.0.8.0" newVersion="3.0.8.0" />
|
||||
</dependentAssembly>
|
||||
<dependentAssembly>
|
||||
<assemblyIdentity name="Microsoft.IdentityModel.JsonWebTokens" publicKeyToken="31bf3856ad364e35" culture="neutral" />
|
||||
|
||||
@@ -60,7 +60,8 @@ namespace EnvelopeGenerator.Infrastructure
|
||||
services.AddDbRepository(opt =>
|
||||
{
|
||||
// scan EnvelopeGenerator
|
||||
opt.RegisterFromAssembly<EGDbContext>(typeof(Config).Assembly);
|
||||
opt.RegisterFromAssembly<EGDbContext>(typeof(EnvelopeReceiver).Assembly);
|
||||
|
||||
// scan UserManager
|
||||
opt.RegisterFromAssembly<EGDbContext>(typeof(User).Assembly);
|
||||
#if NET
|
||||
|
||||
@@ -11,7 +11,6 @@ using DigitalData.EmailProfilerDispatcher.Abstraction.Entities;
|
||||
using DigitalData.UserManager.Infrastructure;
|
||||
using DigitalData.UserManager.Infrastructure.Contracts;
|
||||
using DigitalData.Core.Client;
|
||||
using EnvelopeGenerator.Application.Common.Configurations;
|
||||
#elif NETFRAMEWORK
|
||||
using System.Linq;
|
||||
#endif
|
||||
@@ -40,7 +39,7 @@ public abstract class EGDbContextBase : DbContext
|
||||
, IUserManagerDbContext, IMailDbContext
|
||||
#endif
|
||||
{
|
||||
public DbSet<Domain.Entities.Config> Configs { get; set; }
|
||||
public DbSet<Config> Configs { get; set; }
|
||||
|
||||
public DbSet<EnvelopeReceiver> EnvelopeReceivers { get; set; }
|
||||
|
||||
@@ -98,27 +97,6 @@ public abstract class EGDbContextBase : DbContext
|
||||
{
|
||||
_triggers = triggerParamOptions.Value;
|
||||
_logger = logger;
|
||||
|
||||
Configs = Set<Config>();
|
||||
EnvelopeReceivers = Set<EnvelopeReceiver>();
|
||||
Envelopes = Set<Envelope>();
|
||||
DocumentReceiverElements = Set<Signature>();
|
||||
DocumentStatus = Set<DocumentStatus>();
|
||||
EnvelopeDocument = Set<Document>();
|
||||
EnvelopeHistories = Set<History>();
|
||||
EnvelopeTypes = Set<EnvelopeType>();
|
||||
Receivers = Set<Receiver>();
|
||||
|
||||
GroupOfUsers = Set<GroupOfUser>();
|
||||
Groups = Set<Group>();
|
||||
ModuleOfUsers = Set<ModuleOfUser>();
|
||||
Modules = Set<Module>();
|
||||
Users = Set<User>();
|
||||
UserReps = Set<UserRep>();
|
||||
#if NET
|
||||
EMailOuts = Set<EmailOut>();
|
||||
#endif
|
||||
EnvelopeReceiverReadOnlys = Set<EnvelopeReceiverReadOnly>();
|
||||
}
|
||||
|
||||
protected override void OnModelCreating(ModelBuilder modelBuilder)
|
||||
@@ -147,7 +125,7 @@ public abstract class EGDbContextBase : DbContext
|
||||
|
||||
modelBuilder.Entity<Envelope>()
|
||||
.HasMany(e => e.Documents)
|
||||
.WithOne()
|
||||
.WithOne(d => d.Envelope)
|
||||
.HasForeignKey(ed => ed.EnvelopeId);
|
||||
|
||||
modelBuilder.Entity<Envelope>()
|
||||
@@ -215,6 +193,13 @@ public abstract class EGDbContextBase : DbContext
|
||||
.HasForeignKey(ds => ds.ReceiverId);
|
||||
#endregion DocumentStatus
|
||||
|
||||
#region Annotation
|
||||
modelBuilder.Entity<Signature>()
|
||||
.HasMany(signature => signature.Annotations)
|
||||
.WithOne(annot => annot.Element)
|
||||
.HasForeignKey(annot => annot.ElementId);
|
||||
#endregion
|
||||
|
||||
#region Trigger
|
||||
// Configure entities to handle database triggers
|
||||
void AddTrigger<T>() where T : class => _triggers
|
||||
|
||||
@@ -22,8 +22,8 @@
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Dapper" Version="2.1.66" />
|
||||
<PackageReference Include="DigitalData.Core.Abstraction.Application" Version="1.3.5" />
|
||||
<PackageReference Include="DigitalData.Core.Infrastructure" Version="2.4.3" />
|
||||
<PackageReference Include="DigitalData.Core.Abstraction.Application" Version="1.4.0" />
|
||||
<PackageReference Include="DigitalData.Core.Infrastructure" Version="2.4.5" />
|
||||
<PackageReference Include="QuestPDF" Version="2025.7.1" />
|
||||
</ItemGroup>
|
||||
|
||||
|
||||
@@ -2,11 +2,11 @@
|
||||
using iText.Kernel.Pdf.Canvas;
|
||||
using EnvelopeGenerator.Domain.Interfaces;
|
||||
using iText.Kernel.Colors;
|
||||
using iText.Kernel.Geom;
|
||||
using DigitalData.Core.Abstractions.Interfaces;
|
||||
|
||||
#if NETFRAMEWORK
|
||||
using System;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
using System.Collections.Generic;
|
||||
#endif
|
||||
@@ -19,6 +19,15 @@ namespace EnvelopeGenerator.PdfEditor
|
||||
{
|
||||
return new Pdf<MemoryStream, MemoryStream>(new MemoryStream(documentBytes), new MemoryStream());
|
||||
}
|
||||
|
||||
public static Pdf<MemoryStream, MemoryStream> FromMemory(MemoryStream stream, MemoryStream
|
||||
#if NET
|
||||
?
|
||||
#endif
|
||||
outputStream = null)
|
||||
{
|
||||
return new Pdf<MemoryStream, MemoryStream>(stream, outputStream ?? new MemoryStream(), disposeInputStream: false, disposeOutputStream: outputStream is null);
|
||||
}
|
||||
}
|
||||
|
||||
public class Pdf<TInputStream, TOutputStream> : IDisposable, IAsyncDisposable
|
||||
@@ -31,13 +40,18 @@ namespace EnvelopeGenerator.PdfEditor
|
||||
private readonly PdfReader _reader;
|
||||
private readonly PdfWriter _writer;
|
||||
|
||||
public Pdf(TInputStream inputStream, TOutputStream outputStream)
|
||||
private readonly bool _disposeInputStream;
|
||||
private readonly bool _disposeOutputStream;
|
||||
|
||||
public Pdf(TInputStream inputStream, TOutputStream outputStream, bool disposeInputStream = true, bool disposeOutputStream = true)
|
||||
{
|
||||
_inputStream = inputStream;
|
||||
_outputStream = outputStream;
|
||||
_reader = new PdfReader(inputStream);
|
||||
_writer = new PdfWriter(outputStream);
|
||||
_doc = new PdfDocument(_reader, _writer);
|
||||
_disposeInputStream = disposeInputStream;
|
||||
_disposeOutputStream = disposeOutputStream;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -88,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
|
||||
@@ -106,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;
|
||||
|
||||
@@ -149,16 +163,16 @@ namespace EnvelopeGenerator.PdfEditor
|
||||
{
|
||||
// Managed resources
|
||||
_doc?.Close();
|
||||
_inputStream?.Dispose();
|
||||
_outputStream?.Dispose();
|
||||
if (_disposeInputStream) _inputStream?.Dispose();
|
||||
if(_disposeOutputStream) _outputStream?.Dispose();
|
||||
}
|
||||
else
|
||||
{
|
||||
// When called by the finalizer, clean up only unmanaged resources
|
||||
// Unmanaged resources such as PdfDocument, PdfReader, and PdfWriter are already IDisposable; we close them here
|
||||
try { _doc?.Close(); } catch { }
|
||||
try { _inputStream?.Dispose(); } catch { }
|
||||
try { _outputStream?.Dispose(); } catch { }
|
||||
try { if(_disposeInputStream) _inputStream?.Dispose(); } catch { }
|
||||
try { if (_disposeOutputStream) _outputStream?.Dispose(); } catch { }
|
||||
}
|
||||
|
||||
_disposed = true;
|
||||
@@ -170,15 +184,21 @@ namespace EnvelopeGenerator.PdfEditor
|
||||
return;
|
||||
|
||||
_doc?.Close();
|
||||
if (_inputStream is IAsyncDisposable asyncInput)
|
||||
await asyncInput.DisposeAsync();
|
||||
else
|
||||
_inputStream.Dispose();
|
||||
if (_disposeInputStream)
|
||||
{
|
||||
if (_inputStream is IAsyncDisposable asyncInput)
|
||||
await asyncInput.DisposeAsync();
|
||||
else
|
||||
_inputStream.Dispose();
|
||||
}
|
||||
|
||||
if (_outputStream is IAsyncDisposable asyncOutput)
|
||||
await asyncOutput.DisposeAsync();
|
||||
else
|
||||
_outputStream?.Dispose();
|
||||
if (_disposeOutputStream)
|
||||
{
|
||||
if (_outputStream is IAsyncDisposable asyncOutput)
|
||||
await asyncOutput.DisposeAsync();
|
||||
else
|
||||
_outputStream?.Dispose();
|
||||
}
|
||||
|
||||
_disposed = true;
|
||||
GC.SuppressFinalize(this);
|
||||
|
||||
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" />
|
||||
|
||||
@@ -20,8 +20,8 @@
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Bogus" Version="35.6.3" />
|
||||
<PackageReference Include="coverlet.collector" Version="6.0.0" />
|
||||
<PackageReference Include="DigitalData.Core.Abstraction.Application" Version="1.3.5" />
|
||||
<PackageReference Include="DigitalData.Core.Abstractions" Version="4.1.1" />
|
||||
<PackageReference Include="DigitalData.Core.Abstraction.Application" Version="1.4.0" />
|
||||
<PackageReference Include="DigitalData.Core.Abstractions" Version="4.3.0" />
|
||||
<PackageReference Include="DigitalData.Core.API" Version="2.2.1" />
|
||||
<PackageReference Include="DigitalData.Core.Application" Version="3.4.0" />
|
||||
<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)';
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user