Compare commits
32 Commits
feat/serve
...
refactor/w
| Author | SHA1 | Date | |
|---|---|---|---|
| 0cf603fc79 | |||
| e3d2225a44 | |||
| 595bbf3f25 | |||
| ee7d01ed7f | |||
| 85104511fd | |||
| 1b87528cca | |||
| 2913514da8 | |||
| ce7e70162b | |||
| 5e00b6e9ec | |||
| f0b4c8d00c | |||
| abf616c115 | |||
| 42585e1f2c | |||
| ff10fc6b0a | |||
| 7222f7e9a7 | |||
| d913b949e0 | |||
| a35e05d016 | |||
| 8aabc3e233 | |||
| 8b7f99eb76 | |||
| 91e90b322b | |||
| 74e55e9206 | |||
| fc5ad7ea3c | |||
| ff5582c9b5 | |||
| 6523cca945 | |||
| 28d0b6f81c | |||
| 5df38754cb | |||
| c2b7579717 | |||
| ae1ecbc15f | |||
| 4dcfe67038 | |||
| c812d4dc1f | |||
| 8d27718062 | |||
| c746612d80 | |||
| 6dfcc1b2be |
@@ -0,0 +1,17 @@
|
|||||||
|
namespace EnvelopeGenerator.Application.Common.Configurations;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
///
|
||||||
|
/// </summary>
|
||||||
|
public class SignatureFieldParams
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
///
|
||||||
|
/// </summary>
|
||||||
|
public double Width { get; init; } = 1.77;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
///
|
||||||
|
/// </summary>
|
||||||
|
public double Height { get; init; } = 1.96;
|
||||||
|
}
|
||||||
@@ -23,5 +23,5 @@ public record DocumentDto : DocumentDetailsDto
|
|||||||
/// <summary>
|
/// <summary>
|
||||||
/// Gets or sets the binary data of the document, if available.
|
/// Gets or sets the binary data of the document, if available.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public byte[]? ByteData { get; init; }
|
public byte[]? ByteData { get; set; }
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -62,7 +62,7 @@ public record EnvelopeDto : IEnvelope
|
|||||||
/// <summary>
|
/// <summary>
|
||||||
///
|
///
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public int? ContractType { get; set; }
|
public ContractType? ContractType { get; set; }
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Default value is 'de-DE'
|
/// Default value is 'de-DE'
|
||||||
@@ -84,7 +84,7 @@ public record EnvelopeDto : IEnvelope
|
|||||||
/// <summary>
|
/// <summary>
|
||||||
///
|
///
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public int? CertificationType { get; set; }
|
public CertificationType? CertificationType { get; set; }
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
///
|
///
|
||||||
|
|||||||
@@ -1,4 +1,6 @@
|
|||||||
namespace EnvelopeGenerator.Application.Common.Dto;
|
using EnvelopeGenerator.Domain.Constants;
|
||||||
|
|
||||||
|
namespace EnvelopeGenerator.Application.Common.Dto;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Data Transfer Object representing a type of envelope with its configuration settings.
|
/// Data Transfer Object representing a type of envelope with its configuration settings.
|
||||||
@@ -28,7 +30,7 @@ public class EnvelopeTypeDto
|
|||||||
/// <summary>
|
/// <summary>
|
||||||
/// Gets or sets the certification type identifier.
|
/// Gets or sets the certification type identifier.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public int? CertificationType { get; set; }
|
public CertificationType? CertificationType { get; set; }
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Gets or sets a value indicating whether an access code is required.
|
/// Gets or sets a value indicating whether an access code is required.
|
||||||
@@ -78,5 +80,5 @@ public class EnvelopeTypeDto
|
|||||||
/// <summary>
|
/// <summary>
|
||||||
/// Gets or sets the contract type associated with the envelope type.
|
/// Gets or sets the contract type associated with the envelope type.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public int? ContractType { get; set; }
|
public ContractType? ContractType { get; set; }
|
||||||
}
|
}
|
||||||
@@ -0,0 +1,12 @@
|
|||||||
|
namespace EnvelopeGenerator.Application.Common.Interfaces;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
///
|
||||||
|
/// </summary>
|
||||||
|
public interface IRequestContext
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
///
|
||||||
|
/// </summary>
|
||||||
|
string? Origin { get; }
|
||||||
|
}
|
||||||
@@ -0,0 +1,98 @@
|
|||||||
|
using EnvelopeGenerator.Application.Common.Extensions;
|
||||||
|
using EnvelopeGenerator.Domain.Constants;
|
||||||
|
|
||||||
|
namespace EnvelopeGenerator.Application.Common.Interfaces.Services.Notification;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
///
|
||||||
|
/// </summary>
|
||||||
|
public record DocumentReceivedNotification : EnvelopeNotificationBase
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
///
|
||||||
|
/// </summary>
|
||||||
|
public required string DocumentTitle { get; init; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
///
|
||||||
|
/// </summary>
|
||||||
|
public required string NameReceiver { get; init; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
///
|
||||||
|
/// </summary>
|
||||||
|
public required string NameSender { get; init; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// [SIGNATURE_TYPE]
|
||||||
|
/// </summary>
|
||||||
|
public required bool IsReadAndConfirm { private get; init; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
///
|
||||||
|
/// </summary>
|
||||||
|
public string SignatureType => IsReadAndConfirm ? "Signieren" : "Lesen und bestätigen";
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
///
|
||||||
|
/// </summary>
|
||||||
|
public required bool AdvancedSignature { private get; init; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
///
|
||||||
|
/// </summary>
|
||||||
|
public string DocumentProcess => AdvancedSignature ? " und elektronisch unterschreiben" : string.Empty;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
///
|
||||||
|
/// </summary>
|
||||||
|
public required Uri Origin { private get; init; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
///
|
||||||
|
/// </summary>
|
||||||
|
public required string EnvelopeUuid { private get; init; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
///
|
||||||
|
/// </summary>
|
||||||
|
public required string ReceiverSignature { private get; init; }
|
||||||
|
|
||||||
|
private Uri EnvelopeURL => new(Origin, $"Envelope/{(EnvelopeUuid, ReceiverSignature).ToEnvelopeKey()}");
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
///
|
||||||
|
/// </summary>
|
||||||
|
public string LinkToDocument => EnvelopeURL.AbsoluteUri;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
///
|
||||||
|
/// </summary>
|
||||||
|
public string LinkToDocumentText
|
||||||
|
{
|
||||||
|
get
|
||||||
|
{
|
||||||
|
var urlText = EnvelopeURL.ToString();
|
||||||
|
return $"{urlText[..Math.Min(urlText.Length, 40)]}..";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
///
|
||||||
|
/// </summary>
|
||||||
|
public required string Message { get; init; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
///
|
||||||
|
/// </summary>
|
||||||
|
public string NamePortal { get; set; } = "signFLOW Portal";
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
///
|
||||||
|
/// </summary>
|
||||||
|
/// <returns></returns>
|
||||||
|
public override EmailTemplateType GetTemplateType()
|
||||||
|
{
|
||||||
|
return EmailTemplateType.DocumentReceived;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,9 +1,11 @@
|
|||||||
namespace EnvelopeGenerator.Application.Common.Interfaces.Services.Notification;
|
using EnvelopeGenerator.Domain.Constants;
|
||||||
|
|
||||||
|
namespace EnvelopeGenerator.Application.Common.Interfaces.Services.Notification;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
///
|
///
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public record DocumentSignedNotification
|
public record DocumentSignedNotification : EnvelopeNotificationBase
|
||||||
{
|
{
|
||||||
/// <summary>
|
/// <summary>
|
||||||
///
|
///
|
||||||
@@ -23,7 +25,27 @@ public record DocumentSignedNotification
|
|||||||
/// <summary>
|
/// <summary>
|
||||||
///
|
///
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public required byte[] Attachment { get; init; }
|
public required bool IsReadAndConfirm { private get; init; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
///
|
||||||
|
/// </summary>
|
||||||
|
public string ReceiverAction => IsReadAndConfirm ? "bestätigt" : "unterschrieben";
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
///
|
||||||
|
/// </summary>
|
||||||
|
public string FinalStatus => IsReadAndConfirm ? "Lesebestätigung" : "Signatur";
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
///
|
||||||
|
/// </summary>
|
||||||
|
public string FinalAction => IsReadAndConfirm ? "Empfänger bestätigt" : "Vertragspartner unterzeichnet";
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
///
|
||||||
|
/// </summary>
|
||||||
|
public override EmailTemplateType GetTemplateType() => EmailTemplateType.DocumentSigned;
|
||||||
}
|
}
|
||||||
|
|
||||||
/*
|
/*
|
||||||
|
|||||||
@@ -0,0 +1,19 @@
|
|||||||
|
using EnvelopeGenerator.Domain.Constants;
|
||||||
|
|
||||||
|
namespace EnvelopeGenerator.Application.Common.Interfaces.Services.Notification;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
///
|
||||||
|
/// </summary>
|
||||||
|
public abstract record EnvelopeNotificationBase
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
///
|
||||||
|
/// </summary>
|
||||||
|
public abstract EmailTemplateType GetTemplateType();
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
///
|
||||||
|
/// </summary>
|
||||||
|
public IReadOnlyList<(string FileName, byte[] Content)>? Attachments { get; init; }
|
||||||
|
}
|
||||||
@@ -8,9 +8,11 @@ public interface IEnvelopeNotificationService
|
|||||||
/// <summary>
|
/// <summary>
|
||||||
///
|
///
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="email"></param>
|
/// <typeparam name="TEnvelopeNotificationBase"></typeparam>
|
||||||
/// <param name="documentSigned"></param>
|
/// <param name="receiverEmail"></param>
|
||||||
|
/// <param name="notification"></param>
|
||||||
/// <param name="cancel"></param>
|
/// <param name="cancel"></param>
|
||||||
/// <returns></returns>
|
/// <returns></returns>
|
||||||
public Task Notify(string email, DocumentSignedNotification documentSigned, CancellationToken cancel = default);
|
public Task Notify<TEnvelopeNotificationBase>(string receiverEmail, TEnvelopeNotificationBase notification, CancellationToken cancel = default)
|
||||||
|
where TEnvelopeNotificationBase : EnvelopeNotificationBase;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,6 +2,7 @@
|
|||||||
using DigitalData.EmailProfilerDispatcher.Abstraction.Entities;
|
using DigitalData.EmailProfilerDispatcher.Abstraction.Entities;
|
||||||
using EnvelopeGenerator.Application.Common.Configurations;
|
using EnvelopeGenerator.Application.Common.Configurations;
|
||||||
using EnvelopeGenerator.Application.Common.Extensions;
|
using EnvelopeGenerator.Application.Common.Extensions;
|
||||||
|
using EnvelopeGenerator.Application.Common.Notifications.EnvelopeDeleted;
|
||||||
using EnvelopeGenerator.Domain.Constants;
|
using EnvelopeGenerator.Domain.Constants;
|
||||||
using EnvelopeGenerator.Domain.Entities;
|
using EnvelopeGenerator.Domain.Entities;
|
||||||
using MediatR;
|
using MediatR;
|
||||||
@@ -96,6 +97,10 @@ public abstract class SendMailHandler<TNotification> : INotificationHandler<TNot
|
|||||||
/// <exception cref="NotImplementedException"></exception>
|
/// <exception cref="NotImplementedException"></exception>
|
||||||
public virtual async Task Handle(TNotification notification, CancellationToken cancel)
|
public virtual async Task Handle(TNotification notification, CancellationToken cancel)
|
||||||
{
|
{
|
||||||
|
// do not send email if the envelope is deleted and the status is less than or equal to EnvelopeSaved
|
||||||
|
if (notification is EnvelopeDeletedNotification delNot && delNot.Envelope.Status <= EnvelopeStatus.EnvelopeSaved)
|
||||||
|
return;
|
||||||
|
|
||||||
var placeHolders = CreatePlaceHolders(notification);
|
var placeHolders = CreatePlaceHolders(notification);
|
||||||
|
|
||||||
var temp = await TempRepo
|
var temp = await TempRepo
|
||||||
|
|||||||
@@ -49,6 +49,7 @@ public static class DependencyInjection
|
|||||||
|
|
||||||
services.Configure<DispatcherParams>(config.GetSection(nameof(DispatcherParams)));
|
services.Configure<DispatcherParams>(config.GetSection(nameof(DispatcherParams)));
|
||||||
services.Configure<MailParams>(config.GetSection(nameof(MailParams)));
|
services.Configure<MailParams>(config.GetSection(nameof(MailParams)));
|
||||||
|
services.Configure<SignatureFieldParams>(config.GetSection(nameof(SignatureFieldParams)));
|
||||||
services.Configure<AuthenticatorParams>(config.GetSection(nameof(AuthenticatorParams)));
|
services.Configure<AuthenticatorParams>(config.GetSection(nameof(AuthenticatorParams)));
|
||||||
services.Configure<TotpSmsParams>(config.GetSection(nameof(TotpSmsParams)));
|
services.Configure<TotpSmsParams>(config.GetSection(nameof(TotpSmsParams)));
|
||||||
services.Configure<ESignParams>(config.GetSection(nameof(ESignParams)));
|
services.Configure<ESignParams>(config.GetSection(nameof(ESignParams)));
|
||||||
|
|||||||
@@ -1,5 +1,3 @@
|
|||||||
using AutoMapper;
|
|
||||||
using AutoMapper.QueryableExtensions;
|
|
||||||
using DigitalData.Core.Abstraction.Application.Repository;
|
using DigitalData.Core.Abstraction.Application.Repository;
|
||||||
using EnvelopeGenerator.Application.Common.Dto;
|
using EnvelopeGenerator.Application.Common.Dto;
|
||||||
using EnvelopeGenerator.Domain.Entities;
|
using EnvelopeGenerator.Domain.Entities;
|
||||||
@@ -21,12 +19,10 @@ public record ReadDocumentDetailsQuery(int EnvelopeId, int? UserId = null) : IRe
|
|||||||
public class ReadDocumentDetailsQueryHandler : IRequestHandler<ReadDocumentDetailsQuery, DocumentDetailsDto?>
|
public class ReadDocumentDetailsQueryHandler : IRequestHandler<ReadDocumentDetailsQuery, DocumentDetailsDto?>
|
||||||
{
|
{
|
||||||
private readonly IRepository<Document> _repo;
|
private readonly IRepository<Document> _repo;
|
||||||
private readonly IMapper _mapper;
|
|
||||||
|
|
||||||
public ReadDocumentDetailsQueryHandler(IRepository<Document> repo, IMapper mapper)
|
public ReadDocumentDetailsQueryHandler(IRepository<Document> repo)
|
||||||
{
|
{
|
||||||
_repo = repo;
|
_repo = repo;
|
||||||
_mapper = mapper;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task<DocumentDetailsDto?> Handle(ReadDocumentDetailsQuery query, CancellationToken cancel)
|
public async Task<DocumentDetailsDto?> Handle(ReadDocumentDetailsQuery query, CancellationToken cancel)
|
||||||
@@ -36,9 +32,71 @@ public class ReadDocumentDetailsQueryHandler : IRequestHandler<ReadDocumentDetai
|
|||||||
if (query.UserId is int userId)
|
if (query.UserId is int userId)
|
||||||
docQuery = docQuery.Where(doc => doc.Envelope.UserId == userId);
|
docQuery = docQuery.Where(doc => doc.Envelope.UserId == userId);
|
||||||
|
|
||||||
return await docQuery
|
// shortcut: manual select avoids AutoMapper ProjectTo pulling BYTE_DATA and the full navigation
|
||||||
|
// graph (Envelope → History → User etc.) which caused an extremely slow query (~87s).
|
||||||
|
// Annotations navigation is IEnumerable<T> on the entity (not ICollection<T>), so EF Core
|
||||||
|
// cannot translate it in a Select projection — we skip it entirely here; sender editor only
|
||||||
|
// needs element coordinates, not annotation payloads.
|
||||||
|
var raw = await docQuery
|
||||||
.Where(d => d.EnvelopeId == query.EnvelopeId)
|
.Where(d => d.EnvelopeId == query.EnvelopeId)
|
||||||
.ProjectTo<DocumentDetailsDto>(_mapper.ConfigurationProvider)
|
.Select(d => new
|
||||||
|
{
|
||||||
|
d.Id,
|
||||||
|
d.EnvelopeId,
|
||||||
|
d.AddedWhen,
|
||||||
|
Elements = d.Elements == null ? null : d.Elements.Select(e => new
|
||||||
|
{
|
||||||
|
e.Id,
|
||||||
|
e.DocumentId,
|
||||||
|
e.ReceiverId,
|
||||||
|
e.ElementType,
|
||||||
|
e.X,
|
||||||
|
e.Y,
|
||||||
|
e.Width,
|
||||||
|
e.Height,
|
||||||
|
e.Page,
|
||||||
|
e.Required,
|
||||||
|
e.Tooltip,
|
||||||
|
e.ReadOnly,
|
||||||
|
e.AnnotationIndex,
|
||||||
|
e.AddedWhen,
|
||||||
|
e.ChangedWhen,
|
||||||
|
e.FullName,
|
||||||
|
e.Position,
|
||||||
|
e.Place,
|
||||||
|
}).ToList()
|
||||||
|
})
|
||||||
.FirstOrDefaultAsync(cancel);
|
.FirstOrDefaultAsync(cancel);
|
||||||
|
|
||||||
|
if (raw is null)
|
||||||
|
return null;
|
||||||
|
|
||||||
|
return new DocumentDetailsDto
|
||||||
|
{
|
||||||
|
Id = raw.Id,
|
||||||
|
EnvelopeId = raw.EnvelopeId,
|
||||||
|
AddedWhen = raw.AddedWhen,
|
||||||
|
Elements = raw.Elements?.Select(e => new DocReceiverElementDto
|
||||||
|
{
|
||||||
|
Id = e.Id,
|
||||||
|
DocumentId = e.DocumentId,
|
||||||
|
ReceiverId = e.ReceiverId,
|
||||||
|
ElementType = e.ElementType,
|
||||||
|
X = e.X,
|
||||||
|
Y = e.Y,
|
||||||
|
Width = e.Width,
|
||||||
|
Height = e.Height,
|
||||||
|
Page = e.Page,
|
||||||
|
Required = e.Required,
|
||||||
|
Tooltip = e.Tooltip,
|
||||||
|
ReadOnly = e.ReadOnly,
|
||||||
|
AnnotationIndex = e.AnnotationIndex,
|
||||||
|
AddedWhen = e.AddedWhen,
|
||||||
|
ChangedWhen = e.ChangedWhen,
|
||||||
|
FullName = e.FullName,
|
||||||
|
Position = e.Position,
|
||||||
|
Place = e.Place,
|
||||||
|
})
|
||||||
|
};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,84 @@
|
|||||||
|
using DigitalData.Core.Abstraction.Application.Repository;
|
||||||
|
using DigitalData.Core.Exceptions;
|
||||||
|
using EnvelopeGenerator.Domain.Constants;
|
||||||
|
using EnvelopeGenerator.Domain.Entities;
|
||||||
|
using EnvelopeGenerator.Domain.Exceptions;
|
||||||
|
using MediatR;
|
||||||
|
using Microsoft.EntityFrameworkCore;
|
||||||
|
|
||||||
|
namespace EnvelopeGenerator.Application.EnvelopeReceivers.Commands;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
///
|
||||||
|
/// </summary>
|
||||||
|
public record RemoveEnvelopeReceiverCommand : IRequest
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
///
|
||||||
|
/// </summary>
|
||||||
|
public required int EnvelopeId { get; init; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
///
|
||||||
|
/// </summary>
|
||||||
|
public required int ReceiverId { get; init; }
|
||||||
|
|
||||||
|
internal int? UserId { get; private set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
///
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="userId"></param>
|
||||||
|
/// <returns></returns>
|
||||||
|
public RemoveEnvelopeReceiverCommand Authenticated(int userId)
|
||||||
|
{
|
||||||
|
UserId = userId;
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
///
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="Repo"></param>
|
||||||
|
public class RemoveEnvelopeReceiverCommandHandler(
|
||||||
|
IRepository<EnvelopeReceiver> Repo,
|
||||||
|
IRepository<DocReceiverElement> ElementRepo,
|
||||||
|
IRepository<ElementAnnotation> AnnotRepo) : IRequestHandler<RemoveEnvelopeReceiverCommand>
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
///
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="request"></param>
|
||||||
|
/// <param name="cancel"></param>
|
||||||
|
/// <returns></returns>
|
||||||
|
/// <exception cref="NotImplementedException"></exception>
|
||||||
|
public async Task Handle(RemoveEnvelopeReceiverCommand request, CancellationToken cancel)
|
||||||
|
{
|
||||||
|
var er = await Repo.Query
|
||||||
|
.Where(er => er.EnvelopeId == request.EnvelopeId && er.ReceiverId == request.ReceiverId)
|
||||||
|
.Include(er => er.Envelope)
|
||||||
|
.SingleOrDefaultAsync(cancel)
|
||||||
|
?? throw new NotFoundException();
|
||||||
|
|
||||||
|
if (er.Envelope is null)
|
||||||
|
throw new InvalidOperationException($"In {nameof(RemoveEnvelopeReceiverCommandHandler)} EnvelopeReceiver.Envelope is null." +
|
||||||
|
$"This should not happen." +
|
||||||
|
$"EnvelopeId: {request.EnvelopeId}, ReceiverId: {request.ReceiverId}");
|
||||||
|
|
||||||
|
if (request.UserId is int userId && er.Envelope.UserId != userId)
|
||||||
|
throw new AuthorizationException("Cannot remove receiver from envelope that you do not own.");
|
||||||
|
|
||||||
|
if (er.Envelope.Status > EnvelopeStatus.EnvelopeSaved)
|
||||||
|
throw new ForbiddenException("Cannot remove receiver from envelope that is already processed.");
|
||||||
|
|
||||||
|
// 1. Delete all ElementAnnotations for this EnvelopeReceiver
|
||||||
|
await AnnotRepo.DeleteAsync(ann => ann.Element.Document.EnvelopeId == request.EnvelopeId && ann.Element.ReceiverId == request.ReceiverId, cancel);
|
||||||
|
|
||||||
|
// 2. Delete all DocReceiverElements for this EnvelopeReceiver
|
||||||
|
await ElementRepo.DeleteAsync(el => el.Document.EnvelopeId == request.EnvelopeId && el.ReceiverId == request.ReceiverId, cancel);
|
||||||
|
|
||||||
|
// 3. Delete the EnvelopeReceiver
|
||||||
|
await Repo.DeleteAsync(er => er.EnvelopeId == request.EnvelopeId && er.ReceiverId == request.ReceiverId, cancel);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -45,7 +45,7 @@ public class ReadEnvelopeTypesQueryHandler : IRequestHandler<ReadEnvelopeTypesQu
|
|||||||
Title = t.Title ?? string.Empty,
|
Title = t.Title ?? string.Empty,
|
||||||
Language = string.IsNullOrWhiteSpace(t.Language) ? "de-DE" : t.Language,
|
Language = string.IsNullOrWhiteSpace(t.Language) ? "de-DE" : t.Language,
|
||||||
ExpiresDays = t.ExpiresDays,
|
ExpiresDays = t.ExpiresDays,
|
||||||
CertificationType = (int?)t.CertificationType,
|
CertificationType = t.CertificationType,
|
||||||
UseAccessCode = t.UseAccessCode,
|
UseAccessCode = t.UseAccessCode,
|
||||||
FinalEmailToCreator = (int?)t.FinalEmailToCreator,
|
FinalEmailToCreator = (int?)t.FinalEmailToCreator,
|
||||||
FinalEmailToReceivers = (int?)t.FinalEmailToReceivers,
|
FinalEmailToReceivers = (int?)t.FinalEmailToReceivers,
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
using EnvelopeGenerator.Application.Common.Dto;
|
using EnvelopeGenerator.Application.Common.Dto;
|
||||||
using EnvelopeGenerator.Application.Common.Interfaces.SQLExecutor;
|
using EnvelopeGenerator.Application.Common.Interfaces.SQLExecutor;
|
||||||
|
using EnvelopeGenerator.Domain.Constants;
|
||||||
using MediatR;
|
using MediatR;
|
||||||
using System.ComponentModel.DataAnnotations;
|
using System.ComponentModel.DataAnnotations;
|
||||||
using System.ComponentModel.DataAnnotations.Schema;
|
using System.ComponentModel.DataAnnotations.Schema;
|
||||||
@@ -35,7 +36,7 @@ public record CreateEnvelopeCommand : IRequest<EnvelopeDto?>
|
|||||||
/// <summary>
|
/// <summary>
|
||||||
/// Zertifizierungstyp fuer den Signaturprozess.
|
/// Zertifizierungstyp fuer den Signaturprozess.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public int? CertificationType { get; set; }
|
public CertificationType? CertificationType { get; set; }
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Finale E-Mail-Einstellung fuer den Ersteller.
|
/// Finale E-Mail-Einstellung fuer den Ersteller.
|
||||||
@@ -129,10 +130,27 @@ public record CreateEnvelopeCommand : IRequest<EnvelopeDto?>
|
|||||||
public bool UseSQLExecutor { get; set; } = true;
|
public bool UseSQLExecutor { get; set; } = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
///
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="DataAsBase64"></param>
|
||||||
public record CreateEnvelopeDocumentDto(string DataAsBase64);
|
public record CreateEnvelopeDocumentDto(string DataAsBase64);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
///
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="X"></param>
|
||||||
|
/// <param name="Y"></param>
|
||||||
|
/// <param name="Page"></param>
|
||||||
public record CreateEnvelopeFieldDto(double X, double Y, int Page);
|
public record CreateEnvelopeFieldDto(double X, double Y, int Page);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
///
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="EmailAddress"></param>
|
||||||
|
/// <param name="Name"></param>
|
||||||
|
/// <param name="PhoneNumber"></param>
|
||||||
|
/// <param name="Fields"></param>
|
||||||
public record CreateEnvelopeReceiverDto(
|
public record CreateEnvelopeReceiverDto(
|
||||||
string EmailAddress,
|
string EmailAddress,
|
||||||
string? Name,
|
string? Name,
|
||||||
|
|||||||
@@ -1,68 +1,66 @@
|
|||||||
using AutoMapper;
|
using AutoMapper;
|
||||||
using MediatR;
|
|
||||||
using Microsoft.Extensions.DependencyInjection;
|
|
||||||
using Microsoft.Extensions.Options;
|
|
||||||
using DigitalData.Core.Abstraction.Application.Repository;
|
using DigitalData.Core.Abstraction.Application.Repository;
|
||||||
using DigitalData.EmailProfilerDispatcher.Abstraction.Entities;
|
|
||||||
using DigitalData.Core.Exceptions;
|
using DigitalData.Core.Exceptions;
|
||||||
using EnvelopeGenerator.Domain.Entities;
|
using DigitalData.EmailProfilerDispatcher.Abstraction.Entities;
|
||||||
using EnvelopeGenerator.Application.Common.Dto;
|
using DigitalData.UserManager.Domain.Entities;
|
||||||
using EnvelopeGenerator.Application.Common.Configurations;
|
using EnvelopeGenerator.Application.Common.Configurations;
|
||||||
|
using EnvelopeGenerator.Application.Common.Dto;
|
||||||
using EnvelopeGenerator.Application.Common.Extensions;
|
using EnvelopeGenerator.Application.Common.Extensions;
|
||||||
|
using EnvelopeGenerator.Application.Common.Interfaces;
|
||||||
using EnvelopeGenerator.Application.Common.Interfaces.Services;
|
using EnvelopeGenerator.Application.Common.Interfaces.Services;
|
||||||
|
using EnvelopeGenerator.Application.Common.Interfaces.Services.Notification;
|
||||||
using EnvelopeGenerator.Application.Common.Interfaces.SQLExecutor;
|
using EnvelopeGenerator.Application.Common.Interfaces.SQLExecutor;
|
||||||
using EnvelopeGenerator.Application.Histories.Commands;
|
using EnvelopeGenerator.Application.Histories.Commands;
|
||||||
using EnvelopeGenerator.Domain.Constants;
|
|
||||||
using EnvelopeGenerator.Domain;
|
using EnvelopeGenerator.Domain;
|
||||||
|
using EnvelopeGenerator.Domain.Constants;
|
||||||
|
using EnvelopeGenerator.Domain.Entities;
|
||||||
|
using EnvelopeGenerator.Domain.Interfaces;
|
||||||
|
using MediatR;
|
||||||
using Microsoft.EntityFrameworkCore;
|
using Microsoft.EntityFrameworkCore;
|
||||||
|
using Microsoft.Extensions.DependencyInjection;
|
||||||
|
using Microsoft.Extensions.Logging;
|
||||||
|
using Microsoft.Extensions.Options;
|
||||||
|
|
||||||
namespace EnvelopeGenerator.Application.Envelopes.Commands;
|
namespace EnvelopeGenerator.Application.Envelopes.Commands;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
///
|
///
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public class CreateEnvelopeCommandHandler : IRequestHandler<CreateEnvelopeCommand, EnvelopeDto?>
|
/// <param name="Provider"></param>
|
||||||
|
/// <param name="Mapper"></param>
|
||||||
|
/// <param name="EnvNtfService"></param>
|
||||||
|
/// <param name="ReqCntxt"></param>
|
||||||
|
/// <param name="Logger"></param>
|
||||||
|
public class CreateEnvelopeCommandHandler(IServiceProvider Provider, IMapper Mapper, IEnvelopeNotificationService EnvNtfService, IRequestContext ReqCntxt, ILogger<CreateEnvelopeCommandHandler> Logger) : IRequestHandler<CreateEnvelopeCommand, EnvelopeDto?>
|
||||||
{
|
{
|
||||||
private readonly IServiceProvider _provider;
|
private IEnvelopeExecutor Executor => Provider.GetRequiredService<IEnvelopeExecutor>();
|
||||||
|
|
||||||
private IEnvelopeExecutor Executor => _provider.GetRequiredService<IEnvelopeExecutor>();
|
private IRepository<Envelope> Repository => Provider.GetRequiredService<IRepository<Envelope>>();
|
||||||
|
|
||||||
private IRepository<Envelope> Repository => _provider.GetRequiredService<IRepository<Envelope>>();
|
private IRepository<Document> DocumentRepository => Provider.GetRequiredService<IRepository<Document>>();
|
||||||
|
|
||||||
private IRepository<Document> DocumentRepository => _provider.GetRequiredService<IRepository<Document>>();
|
private IRepository<EnvelopeReceiver> EnvelopeReceiverRepository => Provider.GetRequiredService<IRepository<EnvelopeReceiver>>();
|
||||||
|
|
||||||
private IRepository<EnvelopeReceiver> EnvelopeReceiverRepository => _provider.GetRequiredService<IRepository<EnvelopeReceiver>>();
|
private IRepository<EnvelopeType> EnvelopeTypeRepository => Provider.GetRequiredService<IRepository<EnvelopeType>>();
|
||||||
|
|
||||||
private IRepository<EnvelopeType> EnvelopeTypeRepository => _provider.GetRequiredService<IRepository<EnvelopeType>>();
|
private IRepository<Receiver> ReceiverRepository => Provider.GetRequiredService<IRepository<Receiver>>();
|
||||||
|
|
||||||
private IRepository<Receiver> ReceiverRepository => _provider.GetRequiredService<IRepository<Receiver>>();
|
private IRepository<DocReceiverElement> DocReceiverElementRepository => Provider.GetRequiredService<IRepository<DocReceiverElement>>();
|
||||||
|
|
||||||
private IRepository<DocReceiverElement> DocReceiverElementRepository => _provider.GetRequiredService<IRepository<DocReceiverElement>>();
|
private IRepository<EmailTemplate> EmailTemplateRepository => Provider.GetRequiredService<IRepository<EmailTemplate>>();
|
||||||
|
|
||||||
private IRepository<EmailTemplate> EmailTemplateRepository => _provider.GetRequiredService<IRepository<EmailTemplate>>();
|
private IRepository<EmailOut> EmailOutRepository => Provider.GetRequiredService<IRepository<EmailOut>>();
|
||||||
|
|
||||||
private IRepository<EmailOut> EmailOutRepository => _provider.GetRequiredService<IRepository<EmailOut>>();
|
private MailParams MailParams => Provider.GetRequiredService<IOptions<MailParams>>().Value;
|
||||||
|
|
||||||
private MailParams MailParams => _provider.GetRequiredService<IOptions<MailParams>>().Value;
|
private DispatcherParams DispatcherParams => Provider.GetRequiredService<IOptions<DispatcherParams>>().Value;
|
||||||
|
|
||||||
private DispatcherParams DispatcherParams => _provider.GetRequiredService<IOptions<DispatcherParams>>().Value;
|
private SignatureFieldParams SignatureFieldParams => Provider.GetRequiredService<IOptionsMonitor<SignatureFieldParams>>().CurrentValue;
|
||||||
|
|
||||||
private IConfigService ConfigService => _provider.GetRequiredService<IConfigService>();
|
[Obsolete("Use IConfigService instead.")]
|
||||||
|
private IConfigService ConfigService => Provider.GetRequiredService<IConfigService>();
|
||||||
|
|
||||||
private IMediator Mediator => _provider.GetRequiredService<IMediator>();
|
private IMediator Mediator => Provider.GetRequiredService<IMediator>();
|
||||||
|
|
||||||
private readonly IMapper _mapper;
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
///
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="provider"></param>
|
|
||||||
/// <param name="mapper"></param>
|
|
||||||
public CreateEnvelopeCommandHandler(IServiceProvider provider, IMapper mapper)
|
|
||||||
{
|
|
||||||
_provider = provider;
|
|
||||||
_mapper = mapper;
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
///
|
///
|
||||||
@@ -230,7 +228,7 @@ public class CreateEnvelopeCommandHandler : IRequestHandler<CreateEnvelopeComman
|
|||||||
.ThenInclude(er => er.Receiver)
|
.ThenInclude(er => er.Receiver)
|
||||||
.FirstAsync(e => e.Id == envelope.Id, cancel);
|
.FirstAsync(e => e.Id == envelope.Id, cancel);
|
||||||
|
|
||||||
return _mapper.Map<EnvelopeDto>(envelope);
|
return Mapper.Map<EnvelopeDto>(envelope);
|
||||||
}
|
}
|
||||||
|
|
||||||
private async Task<EnvelopeSettings> ResolveEnvelopeSettingsAsync(CreateEnvelopeCommand request, Envelope? existing, CancellationToken cancel)
|
private async Task<EnvelopeSettings> ResolveEnvelopeSettingsAsync(CreateEnvelopeCommand request, Envelope? existing, CancellationToken cancel)
|
||||||
@@ -268,9 +266,9 @@ public class CreateEnvelopeCommandHandler : IRequestHandler<CreateEnvelopeComman
|
|||||||
Language: ResolveLanguage(request.Language, selectedType?.Language, existing?.Language),
|
Language: ResolveLanguage(request.Language, selectedType?.Language, existing?.Language),
|
||||||
UseAccessCode: useAccessCode,
|
UseAccessCode: useAccessCode,
|
||||||
CertificationType: request.CertificationType
|
CertificationType: request.CertificationType
|
||||||
?? (int?)selectedType?.CertificationType
|
?? selectedType?.CertificationType
|
||||||
?? existing?.CertificationType
|
?? existing?.CertificationType
|
||||||
?? (int)CertificationType.AdvancedElectronicSignature,
|
?? CertificationType.AdvancedElectronicSignature,
|
||||||
FinalEmailToCreator: request.FinalEmailToCreator
|
FinalEmailToCreator: request.FinalEmailToCreator
|
||||||
?? (int?)selectedType?.FinalEmailToCreator
|
?? (int?)selectedType?.FinalEmailToCreator
|
||||||
?? existing?.FinalEmailToCreator
|
?? existing?.FinalEmailToCreator
|
||||||
@@ -299,7 +297,7 @@ public class CreateEnvelopeCommandHandler : IRequestHandler<CreateEnvelopeComman
|
|||||||
ExpiresWarningWhen: expiresWarningWhenDays > 0 ? DateTime.Now.AddDays(expiresWarningWhenDays) : null,
|
ExpiresWarningWhen: expiresWarningWhenDays > 0 ? DateTime.Now.AddDays(expiresWarningWhenDays) : null,
|
||||||
ContractType: existing?.ContractType
|
ContractType: existing?.ContractType
|
||||||
?? selectedType?.ContractType
|
?? selectedType?.ContractType
|
||||||
?? (int)ContractType.Contract);
|
?? ContractType.Contract);
|
||||||
}
|
}
|
||||||
|
|
||||||
private static string ResolveLanguage(string? preferred, string? fallback1, string? fallback2)
|
private static string ResolveLanguage(string? preferred, string? fallback1, string? fallback2)
|
||||||
@@ -320,7 +318,7 @@ public class CreateEnvelopeCommandHandler : IRequestHandler<CreateEnvelopeComman
|
|||||||
int? EnvelopeTypeId,
|
int? EnvelopeTypeId,
|
||||||
string Language,
|
string Language,
|
||||||
bool UseAccessCode,
|
bool UseAccessCode,
|
||||||
int CertificationType,
|
CertificationType CertificationType,
|
||||||
int FinalEmailToCreator,
|
int FinalEmailToCreator,
|
||||||
int FinalEmailToReceivers,
|
int FinalEmailToReceivers,
|
||||||
bool SendReminderEmails,
|
bool SendReminderEmails,
|
||||||
@@ -330,7 +328,7 @@ public class CreateEnvelopeCommandHandler : IRequestHandler<CreateEnvelopeComman
|
|||||||
int ExpiresWarningWhenDays,
|
int ExpiresWarningWhenDays,
|
||||||
DateTime? ExpiresWhen,
|
DateTime? ExpiresWhen,
|
||||||
DateTime? ExpiresWarningWhen,
|
DateTime? ExpiresWarningWhen,
|
||||||
int ContractType);
|
ContractType ContractType);
|
||||||
|
|
||||||
private async Task<string> ResolveSenderReferenceAsync(Envelope envelope, CancellationToken cancel)
|
private async Task<string> ResolveSenderReferenceAsync(Envelope envelope, CancellationToken cancel)
|
||||||
{
|
{
|
||||||
@@ -444,8 +442,8 @@ public class CreateEnvelopeCommandHandler : IRequestHandler<CreateEnvelopeComman
|
|||||||
ElementType = 1,
|
ElementType = 1,
|
||||||
X = field.X,
|
X = field.X,
|
||||||
Y = field.Y,
|
Y = field.Y,
|
||||||
Width = 1.77,
|
Width = SignatureFieldParams.Width,
|
||||||
Height = 1.96,
|
Height = SignatureFieldParams.Height,
|
||||||
Page = field.Page,
|
Page = field.Page,
|
||||||
Required = true,
|
Required = true,
|
||||||
ReadOnly = false,
|
ReadOnly = false,
|
||||||
@@ -529,15 +527,15 @@ public class CreateEnvelopeCommandHandler : IRequestHandler<CreateEnvelopeComman
|
|||||||
if (!hasDocument)
|
if (!hasDocument)
|
||||||
throw new BadRequestException("Cannot send envelope without a document.");
|
throw new BadRequestException("Cannot send envelope without a document.");
|
||||||
|
|
||||||
var links = await EnvelopeReceiverRepository.Query
|
var envRcvs = await EnvelopeReceiverRepository.Query
|
||||||
.Include(er => er.Receiver)
|
.Include(er => er.Receiver)
|
||||||
.Where(er => er.EnvelopeId == envelope.Id)
|
.Where(er => er.EnvelopeId == envelope.Id)
|
||||||
.ToListAsync(cancel);
|
.ToListAsync(cancel);
|
||||||
|
|
||||||
if (links.Count == 0)
|
if (envRcvs.Count == 0)
|
||||||
throw new BadRequestException("Cannot send envelope without receivers.");
|
throw new BadRequestException("Cannot send envelope without receivers.");
|
||||||
|
|
||||||
var receiverIds = links.Select(l => l.ReceiverId).Distinct().ToList();
|
var receiverIds = envRcvs.Select(l => l.ReceiverId).Distinct().ToList();
|
||||||
var documentIds = await DocumentRepository.Query
|
var documentIds = await DocumentRepository.Query
|
||||||
.Where(d => d.EnvelopeId == envelope.Id)
|
.Where(d => d.EnvelopeId == envelope.Id)
|
||||||
.Select(d => d.Id)
|
.Select(d => d.Id)
|
||||||
@@ -550,7 +548,7 @@ public class CreateEnvelopeCommandHandler : IRequestHandler<CreateEnvelopeComman
|
|||||||
.Select(g => new { ReceiverId = g.Key, Count = g.Count() })
|
.Select(g => new { ReceiverId = g.Key, Count = g.Count() })
|
||||||
.ToDictionaryAsync(x => x.ReceiverId, x => x.Count, cancel);
|
.ToDictionaryAsync(x => x.ReceiverId, x => x.Count, cancel);
|
||||||
|
|
||||||
var receiversWithoutFields = links
|
var receiversWithoutFields = envRcvs
|
||||||
.Where(link => !receiverFieldCounts.TryGetValue(link.ReceiverId, out var count) || count <= 0)
|
.Where(link => !receiverFieldCounts.TryGetValue(link.ReceiverId, out var count) || count <= 0)
|
||||||
.Select(link => link.Name)
|
.Select(link => link.Name)
|
||||||
.ToList();
|
.ToList();
|
||||||
@@ -574,70 +572,39 @@ public class CreateEnvelopeCommandHandler : IRequestHandler<CreateEnvelopeComman
|
|||||||
Status = EnvelopeStatus.EnvelopeQueued
|
Status = EnvelopeStatus.EnvelopeQueued
|
||||||
}, cancel);
|
}, cancel);
|
||||||
|
|
||||||
await QueueInvitationEmailsAsync(envelope, links, userReference, cancel);
|
await SendInvitationEmailsAsync(envelope, envRcvs, cancel);
|
||||||
|
|
||||||
envelope.Status = EnvelopeStatus.EnvelopeQueued;
|
envelope.Status = EnvelopeStatus.EnvelopeQueued;
|
||||||
}
|
}
|
||||||
|
|
||||||
private async Task QueueInvitationEmailsAsync(
|
private async Task SendInvitationEmailsAsync(
|
||||||
Envelope envelope,
|
Envelope envelope,
|
||||||
IReadOnlyCollection<EnvelopeReceiver> links,
|
IReadOnlyCollection<EnvelopeReceiver> envRcvs,
|
||||||
string senderReference,
|
|
||||||
CancellationToken cancel)
|
CancellationToken cancel)
|
||||||
{
|
{
|
||||||
var template = await EmailTemplateRepository.Query
|
foreach (var er in envRcvs)
|
||||||
.SingleOrDefaultAsync(x => x.Name == EmailTemplateType.DocumentReceived.ToString(), cancel)
|
|
||||||
?? throw new InvalidOperationException($"Email template not found: {EmailTemplateType.DocumentReceived}");
|
|
||||||
|
|
||||||
var signatureHost = await ConfigService.ReadDefaultSignatureHost();
|
|
||||||
var senderName = senderReference;
|
|
||||||
|
|
||||||
foreach (var link in links)
|
|
||||||
{
|
{
|
||||||
var receiver = link.Receiver;
|
var advancedSignature = envelope.CertificationType == CertificationType.AdvancedElectronicSignature;
|
||||||
if (receiver is null || string.IsNullOrWhiteSpace(receiver.EmailAddress) || string.IsNullOrWhiteSpace(receiver.Signature))
|
var origin = new Uri(ReqCntxt.Origin ?? throw new InvalidOperationException($"The request origin is missing in {nameof(CreateEnvelopeCommandHandler)}."));
|
||||||
continue;
|
try
|
||||||
|
|
||||||
var envelopeKey = (envelope.Uuid, receiver.Signature).ToEnvelopeKey();
|
|
||||||
var documentLink = $"{signatureHost.TrimEnd('/')}/envelope/login/{Uri.EscapeDataString(envelopeKey)}";
|
|
||||||
|
|
||||||
var placeholders = new Dictionary<string, string>(MailParams.Placeholders)
|
|
||||||
{
|
{
|
||||||
["[NAME_RECEIVER]"] = link.Name ?? string.Empty,
|
await EnvNtfService.Notify(er.Receiver.EmailAddress, new DocumentReceivedNotification
|
||||||
["[NAME_SENDER]"] = senderName ?? string.Empty,
|
{
|
||||||
["[DOCUMENT_TITLE]"] = envelope.Title ?? string.Empty,
|
AdvancedSignature = advancedSignature,
|
||||||
["[MESSAGE]"] = envelope.Message ?? string.Empty,
|
DocumentTitle = envelope.Title ?? string.Empty,
|
||||||
["[LINK_TO_DOCUMENT]"] = documentLink,
|
NameReceiver = er.Name ?? string.Empty,
|
||||||
["[LINK_TO_DOCUMENT_TEXT]"] = documentLink[..Math.Min(40, documentLink.Length)] + ".."
|
EnvelopeUuid = envelope.Uuid,
|
||||||
};
|
IsReadAndConfirm = envelope.IsReadAndConfirm(),
|
||||||
|
Message = envelope.Message ?? string.Empty,
|
||||||
var subject = ReplacePlaceHolders(template.Subject, placeholders);
|
NameSender = $"{envelope.User.Prename} {envelope.User.Name}",
|
||||||
var body = ReplacePlaceHolders(template.Body, placeholders);
|
Origin = origin,
|
||||||
|
ReceiverSignature = er.Receiver.Signature,
|
||||||
var emailOut = new EmailOut
|
}, cancel);
|
||||||
|
}
|
||||||
|
catch(Exception ex)
|
||||||
{
|
{
|
||||||
EmailAddress = receiver.EmailAddress,
|
Logger.LogError(ex, "Failed to send invitation email for receiver {emailAddress} in envelope {envelopeId}.", er.Receiver.EmailAddress, envelope.Id);
|
||||||
EmailSubj = subject,
|
}
|
||||||
EmailBody = body,
|
|
||||||
AddedWhen = DateTime.Now,
|
|
||||||
AddedWho = DispatcherParams.AddedWho,
|
|
||||||
SendingProfile = DispatcherParams.SendingProfile,
|
|
||||||
ReminderTypeId = DispatcherParams.ReminderTypeId,
|
|
||||||
EmailAttmt1 = DispatcherParams.EmailAttmt1,
|
|
||||||
WfId = (int)EnvelopeStatus.MessageInvitationSent,
|
|
||||||
ReferenceString = receiver.EmailAddress,
|
|
||||||
ReferenceId = link.ReceiverId
|
|
||||||
};
|
|
||||||
|
|
||||||
await EmailOutRepository.CreateAsync(emailOut, cancel);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private static string ReplacePlaceHolders(string text, Dictionary<string, string> placeHolders)
|
|
||||||
{
|
|
||||||
foreach (var placeHolder in placeHolders)
|
|
||||||
text = text.Replace(placeHolder.Key, placeHolder.Value);
|
|
||||||
|
|
||||||
return text;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,6 +2,9 @@
|
|||||||
using EnvelopeGenerator.Domain.Constants;
|
using EnvelopeGenerator.Domain.Constants;
|
||||||
using EnvelopeGenerator.Application.Common.Query;
|
using EnvelopeGenerator.Application.Common.Query;
|
||||||
using EnvelopeGenerator.Application.Common.Dto;
|
using EnvelopeGenerator.Application.Common.Dto;
|
||||||
|
using EnvelopeGenerator.Application.Common.Dto.EnvelopeReceiver;
|
||||||
|
using EnvelopeGenerator.Application.Common.Dto.History;
|
||||||
|
using EnvelopeGenerator.Application.Common.Dto.Receiver;
|
||||||
using AutoMapper;
|
using AutoMapper;
|
||||||
using DigitalData.Core.Abstraction.Application.Repository;
|
using DigitalData.Core.Abstraction.Application.Repository;
|
||||||
using EnvelopeGenerator.Domain.Entities;
|
using EnvelopeGenerator.Domain.Entities;
|
||||||
@@ -14,6 +17,11 @@ namespace EnvelopeGenerator.Application.Envelopes.Queries;
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
public record ReadEnvelopeQuery : EnvelopeQueryBase, IRequest<IEnumerable<EnvelopeDto>>
|
public record ReadEnvelopeQuery : EnvelopeQueryBase, IRequest<IEnumerable<EnvelopeDto>>
|
||||||
{
|
{
|
||||||
|
/// <summary>
|
||||||
|
///
|
||||||
|
/// </summary>
|
||||||
|
public bool IncludeDocAndDocRes { get; init; } = true;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
///
|
///
|
||||||
/// </summary>
|
/// </summary>
|
||||||
@@ -148,6 +156,86 @@ public class ReadEnvelopeQueryHandler : IRequestHandler<ReadEnvelopeQuery, IEnum
|
|||||||
if (request is { OnlyCompleted: true })
|
if (request is { OnlyCompleted: true })
|
||||||
query = query.Where(e => Status.Completed.Contains(e.Status));
|
query = query.Where(e => Status.Completed.Contains(e.Status));
|
||||||
|
|
||||||
|
if (!request.IncludeDocAndDocRes)
|
||||||
|
{
|
||||||
|
return await query
|
||||||
|
.Select(e => new EnvelopeDto
|
||||||
|
{
|
||||||
|
Id = e.Id,
|
||||||
|
UserId = e.UserId,
|
||||||
|
Status = e.Status,
|
||||||
|
StatusName = string.Empty,
|
||||||
|
Uuid = e.Uuid,
|
||||||
|
Message = e.Message,
|
||||||
|
AddedWhen = e.AddedWhen,
|
||||||
|
ChangedWhen = e.ChangedWhen,
|
||||||
|
Title = e.Title ?? string.Empty,
|
||||||
|
ContractType = e.ContractType,
|
||||||
|
Language = e.Language,
|
||||||
|
EnvelopeTypeId = e.EnvelopeTypeId,
|
||||||
|
CertificationType = e.CertificationType,
|
||||||
|
FinalEmailToCreator = e.FinalEmailToCreator,
|
||||||
|
FinalEmailToReceivers = e.FinalEmailToReceivers,
|
||||||
|
SendReminderEmails = e.SendReminderEmails,
|
||||||
|
FirstReminderDays = e.FirstReminderDays,
|
||||||
|
ReminderIntervalDays = e.ReminderIntervalDays,
|
||||||
|
ExpiresWhenDays = e.ExpiresWhenDays,
|
||||||
|
ExpiresWarningWhenDays = e.ExpiresWarningWhenDays,
|
||||||
|
UseAccessCode = e.UseAccessCode ?? true,
|
||||||
|
TFAEnabled = e.TfaEnabled ?? false,
|
||||||
|
EnvelopeTypeTitle = e.Type != null ? e.Type.Title : null,
|
||||||
|
IsAlreadySent = false,
|
||||||
|
DocResult = null,
|
||||||
|
Documents = null,
|
||||||
|
EnvelopeReceivers = e.EnvelopeReceivers.Select(er => new EnvelopeReceiverDto
|
||||||
|
{
|
||||||
|
EnvelopeId = er.EnvelopeId,
|
||||||
|
ReceiverId = er.ReceiverId,
|
||||||
|
Sequence = er.Sequence,
|
||||||
|
Name = er.Name,
|
||||||
|
JobTitle = er.JobTitle,
|
||||||
|
CompanyName = er.CompanyName,
|
||||||
|
PrivateMessage = er.PrivateMessage,
|
||||||
|
AccessCode = er.AccessCode,
|
||||||
|
AddedWhen = er.AddedWhen,
|
||||||
|
ChangedWhen = er.ChangedWhen,
|
||||||
|
HasPhoneNumber = er.HasPhoneNumber,
|
||||||
|
Receiver = er.Receiver == null
|
||||||
|
? null
|
||||||
|
: new ReceiverDto
|
||||||
|
{
|
||||||
|
Id = er.Receiver.Id,
|
||||||
|
EmailAddress = er.Receiver.EmailAddress,
|
||||||
|
Signature = er.Receiver.Signature,
|
||||||
|
AddedWhen = er.Receiver.AddedWhen,
|
||||||
|
TfaRegDeadline = er.Receiver.TfaRegDeadline
|
||||||
|
}
|
||||||
|
}),
|
||||||
|
Histories = e.Histories.Select(h => new HistoryDto
|
||||||
|
{
|
||||||
|
Id = h.Id,
|
||||||
|
EnvelopeId = h.EnvelopeId,
|
||||||
|
UserReference = h.UserReference,
|
||||||
|
Status = h.Status,
|
||||||
|
StatusName = null,
|
||||||
|
AddedWhen = h.AddedWhen,
|
||||||
|
ActionDate = h.ActionDate,
|
||||||
|
Comment = h.Comment,
|
||||||
|
Receiver = h.Receiver == null
|
||||||
|
? null
|
||||||
|
: new ReceiverDto
|
||||||
|
{
|
||||||
|
Id = h.Receiver.Id,
|
||||||
|
EmailAddress = h.Receiver.EmailAddress,
|
||||||
|
Signature = h.Receiver.Signature,
|
||||||
|
AddedWhen = h.Receiver.AddedWhen,
|
||||||
|
TfaRegDeadline = h.Receiver.TfaRegDeadline
|
||||||
|
}
|
||||||
|
})
|
||||||
|
})
|
||||||
|
.ToListAsync(cancel);
|
||||||
|
}
|
||||||
|
|
||||||
var envelopes = await query
|
var envelopes = await query
|
||||||
.Include(e => e.Type)
|
.Include(e => e.Type)
|
||||||
.Include(e => e.EnvelopeReceivers).ThenInclude(er => er.Receiver)
|
.Include(e => e.EnvelopeReceivers).ThenInclude(er => er.Receiver)
|
||||||
|
|||||||
@@ -98,7 +98,8 @@ public class FinalizeEnvelopeCommandHandler(IRepository<Envelope> Repo, ISender
|
|||||||
{
|
{
|
||||||
DocumentTitle = envelope.Title,
|
DocumentTitle = envelope.Title,
|
||||||
NameReceiver = er.Name,
|
NameReceiver = er.Name,
|
||||||
Attachment = doc
|
Attachments = [($"unterzeichnetes-dokument.pdf", doc)],
|
||||||
|
IsReadAndConfirm = envelope.IsReadAndConfirm(),
|
||||||
}, cancel);
|
}, cancel);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -2,8 +2,7 @@
|
|||||||
{
|
{
|
||||||
public enum CertificationType
|
public enum CertificationType
|
||||||
{
|
{
|
||||||
AdvancedElectronicSignature = 1
|
AdvancedElectronicSignature = 1,
|
||||||
// ElectronicSignature = 1
|
ElectronicSignature = 2
|
||||||
// QualifiedSignature = 2
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -30,7 +30,7 @@ namespace EnvelopeGenerator.Domain.Entities
|
|||||||
SendReminderEmails = false;
|
SendReminderEmails = false;
|
||||||
FirstReminderDays = 0;
|
FirstReminderDays = 0;
|
||||||
ReminderIntervalDays = 0;
|
ReminderIntervalDays = 0;
|
||||||
CertificationType = (int)Constants.CertificationType.AdvancedElectronicSignature;
|
CertificationType = Constants.CertificationType.AdvancedElectronicSignature;
|
||||||
UseAccessCode = false;
|
UseAccessCode = false;
|
||||||
Documents = Enumerable.Empty<Document>().ToList();
|
Documents = Enumerable.Empty<Document>().ToList();
|
||||||
Histories = Enumerable.Empty<History>().ToList();
|
Histories = Enumerable.Empty<History>().ToList();
|
||||||
@@ -83,7 +83,7 @@ namespace EnvelopeGenerator.Domain.Entities
|
|||||||
public string Comment { get; set; }
|
public string Comment { get; set; }
|
||||||
|
|
||||||
[Column("CONTRACT_TYPE")]
|
[Column("CONTRACT_TYPE")]
|
||||||
public int? ContractType { get; set; }
|
public ContractType? ContractType { get; set; }
|
||||||
|
|
||||||
#if NETFRAMEWORK
|
#if NETFRAMEWORK
|
||||||
[NotMapped]
|
[NotMapped]
|
||||||
@@ -111,7 +111,7 @@ namespace EnvelopeGenerator.Domain.Entities
|
|||||||
public bool ReadOnly => this.IsReadAndConfirm();
|
public bool ReadOnly => this.IsReadAndConfirm();
|
||||||
|
|
||||||
[Column("CERTIFICATION_TYPE")]
|
[Column("CERTIFICATION_TYPE")]
|
||||||
public int? CertificationType { get; set; }
|
public CertificationType? CertificationType { get; set; }
|
||||||
|
|
||||||
[Column("USE_ACCESS_CODE")]
|
[Column("USE_ACCESS_CODE")]
|
||||||
public bool? UseAccessCode { get; set; }
|
public bool? UseAccessCode { get; set; }
|
||||||
@@ -129,7 +129,11 @@ namespace EnvelopeGenerator.Domain.Entities
|
|||||||
public int? ExpiresWarningWhenDays { get; set; }
|
public int? ExpiresWarningWhenDays { get; set; }
|
||||||
|
|
||||||
[ForeignKey("UserId")]
|
[ForeignKey("UserId")]
|
||||||
public User User { get; set; }
|
public User
|
||||||
|
#if nullable
|
||||||
|
?
|
||||||
|
#endif
|
||||||
|
User { get; set; }
|
||||||
|
|
||||||
[Column("TFA_ENABLED")]
|
[Column("TFA_ENABLED")]
|
||||||
public bool? TfaEnabled { get; set; }
|
public bool? TfaEnabled { get; set; }
|
||||||
|
|||||||
@@ -1,5 +1,7 @@
|
|||||||
using System.ComponentModel.DataAnnotations;
|
using System.ComponentModel.DataAnnotations;
|
||||||
using System.ComponentModel.DataAnnotations.Schema;
|
using System.ComponentModel.DataAnnotations.Schema;
|
||||||
|
using EnvelopeGenerator.Domain.Constants;
|
||||||
|
|
||||||
#if NETFRAMEWORK
|
#if NETFRAMEWORK
|
||||||
using System;
|
using System;
|
||||||
#endif
|
#endif
|
||||||
@@ -56,7 +58,7 @@ namespace EnvelopeGenerator.Domain.Entities
|
|||||||
public int? ReminderIntervalDays { get; set; }
|
public int? ReminderIntervalDays { get; set; }
|
||||||
|
|
||||||
[Column("CONTRACT_TYPE")]
|
[Column("CONTRACT_TYPE")]
|
||||||
public int? ContractType { get; set; }
|
public ContractType? ContractType { get; set; }
|
||||||
|
|
||||||
[NotMapped]
|
[NotMapped]
|
||||||
public bool TFA_Enabled { get; set; } = false;
|
public bool TFA_Enabled { get; set; } = false;
|
||||||
|
|||||||
@@ -0,0 +1,7 @@
|
|||||||
|
namespace EnvelopeGenerator.Infrastructure.Notification.Configuration;
|
||||||
|
|
||||||
|
public enum ConsumerType
|
||||||
|
{
|
||||||
|
API,
|
||||||
|
RabbitMQ
|
||||||
|
}
|
||||||
@@ -0,0 +1,16 @@
|
|||||||
|
using DigitalData.MessagingService.RabbitMQ;
|
||||||
|
|
||||||
|
namespace EnvelopeGenerator.Infrastructure.Notification.Configuration;
|
||||||
|
|
||||||
|
public class NotificationOptions
|
||||||
|
{
|
||||||
|
public const string SectionName = "Notification";
|
||||||
|
|
||||||
|
public ConsumerType ConsumerType { get; set; }
|
||||||
|
|
||||||
|
public string? ApiUrl { get; set; }
|
||||||
|
|
||||||
|
public string SenderEmail { get; set; } = null!;
|
||||||
|
|
||||||
|
public RabbitMqConnectionConfiguration? RabbitMQ { get; set; }
|
||||||
|
}
|
||||||
@@ -1,41 +1,47 @@
|
|||||||
using DigitalData.MessagingService.Client;
|
using DigitalData.MessagingService.Client;
|
||||||
using DigitalData.MessagingService.Domain.Entities;
|
|
||||||
using DigitalData.MessagingService.RabbitMQ;
|
|
||||||
using EnvelopeGenerator.Application.Common.Interfaces.Services.Notification;
|
using EnvelopeGenerator.Application.Common.Interfaces.Services.Notification;
|
||||||
|
using EnvelopeGenerator.Infrastructure.Notification.Configuration;
|
||||||
using Microsoft.Extensions.Configuration;
|
using Microsoft.Extensions.Configuration;
|
||||||
using Microsoft.Extensions.DependencyInjection;
|
using Microsoft.Extensions.DependencyInjection;
|
||||||
using System;
|
|
||||||
using System.Collections.Generic;
|
|
||||||
using System.Linq;
|
|
||||||
using System.Text;
|
|
||||||
using System.Threading.Tasks;
|
|
||||||
|
|
||||||
namespace EnvelopeGenerator.Infrastructure.Notification;
|
namespace EnvelopeGenerator.Infrastructure.Notification;
|
||||||
|
|
||||||
public static class DependencyInjection
|
public static class DependencyInjection
|
||||||
{
|
{
|
||||||
public static IServiceCollection AddNotificationInfrastructure(this IServiceCollection services, IConfiguration configuration)
|
public static IServiceCollection AddNotificationInfrastructure(this IServiceCollection services, IConfiguration configuration, string sectionName = NotificationOptions.SectionName)
|
||||||
{
|
{
|
||||||
var rMqConfig = configuration.GetSection("RabbitMQ").Get<RabbitMqConfiguration>()
|
var ntfConfiguration = configuration.GetSection(sectionName);
|
||||||
?? throw new ArgumentNullException(nameof(RabbitMqConfiguration), "RabbitMQ configuration is missing");
|
services.Configure<NotificationOptions>(ntfConfiguration);
|
||||||
|
|
||||||
EmailSender.ConnectRabbitMq(config =>
|
var options = ntfConfiguration.Get<NotificationOptions>()
|
||||||
|
?? throw new InvalidOperationException(
|
||||||
|
$"Failed to bind configuration section '{sectionName}' to {nameof(NotificationOptions)}. Ensure the section exists and is correctly configured.");
|
||||||
|
|
||||||
|
switch (options.ConsumerType)
|
||||||
{
|
{
|
||||||
config.HostName = rMqConfig.HostName;
|
case ConsumerType.API:
|
||||||
config.Port = rMqConfig.Port;
|
if (options.ApiUrl is string apiUrl)
|
||||||
config.UserName = rMqConfig.UserName;
|
services.AddMessagingServiceHttpClient(opt =>
|
||||||
config.Password = rMqConfig.Password;
|
{
|
||||||
|
opt.BaseAddress = new Uri(apiUrl);
|
||||||
config.VirtualHost = "/";
|
// Additional configuration for the MessagingServiceHttpClient can be added here if needed
|
||||||
config.QueueName = "emailprofiler.email.outbox";
|
});
|
||||||
config.ExchangeName = "emailprofiler.emails";
|
else
|
||||||
config.RoutingKey = "email.outbox";
|
throw new InvalidOperationException(
|
||||||
config.DlqQueueName = "emailprofiler.email.outbox.dlq";
|
$"MessagingServiceApiUrl must be provided when ClientType is set to Api in the '{sectionName}' configuration section.");
|
||||||
config.DlqExchangeName = "emailprofiler.emails.dlq";
|
break;
|
||||||
config.DlqRoutingKey = "email.outbox.dlq";
|
case ConsumerType.RabbitMQ:
|
||||||
});
|
if(options.RabbitMQ is not null)
|
||||||
|
services.AddMessagingServiceClient(ntfConfiguration, nameof(options.RabbitMQ));
|
||||||
services.Configure<EmailAccount>(configuration.GetSection("NotificationEmailAccount"));
|
else
|
||||||
|
throw new InvalidOperationException(
|
||||||
|
$"Missing RabbitMQ connection configuration." +
|
||||||
|
$"When ClientType is set to RabbitMQ, a valid RabbitMQ configuration must be provided in the '{sectionName}' configuration section.");
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
throw new InvalidOperationException(
|
||||||
|
$"Unsupported ConsumerType '{options.ConsumerType}' in the '{sectionName}' configuration section.");
|
||||||
|
}
|
||||||
|
|
||||||
services.AddScoped<IEnvelopeNotificationService, EnvelopeNotificationService>();
|
services.AddScoped<IEnvelopeNotificationService, EnvelopeNotificationService>();
|
||||||
return services;
|
return services;
|
||||||
|
|||||||
@@ -8,7 +8,7 @@
|
|||||||
</PropertyGroup>
|
</PropertyGroup>
|
||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<PackageReference Include="DigitalData.MessagingService.Client" Version="1.0.5" />
|
<PackageReference Include="DigitalData.MessagingService.Client" Version="3.0.1" />
|
||||||
<PackageReference Include="Microsoft.Extensions.Configuration.Binder" Version="10.0.10" />
|
<PackageReference Include="Microsoft.Extensions.Configuration.Binder" Version="10.0.10" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,15 @@
|
|||||||
|
using DigitalData.MessagingService.Application.Common.Dto;
|
||||||
|
|
||||||
|
namespace EnvelopeGenerator.Infrastructure.Notification;
|
||||||
|
|
||||||
|
public static class EnvelopeNotificationExtensions
|
||||||
|
{
|
||||||
|
public static EmailAttachmentDto ToEmailAttachmentDto(this (string FileName, byte[] Content) attachment) => new()
|
||||||
|
{
|
||||||
|
FileName = attachment.FileName,
|
||||||
|
Content = attachment.Content
|
||||||
|
};
|
||||||
|
|
||||||
|
public static IEnumerable<EmailAttachmentDto> ToEmailAttachmentDto(this IEnumerable<(string FileName, byte[] Content)> attachments)
|
||||||
|
=> attachments.Select(ToEmailAttachmentDto);
|
||||||
|
}
|
||||||
@@ -1,44 +1,45 @@
|
|||||||
using DigitalData.Core.Abstraction.Application.Repository;
|
using DigitalData.Core.Abstraction.Application.Repository;
|
||||||
using DigitalData.Core.Exceptions;
|
using DigitalData.MessagingService.Application.Common.Interfaces;
|
||||||
using DigitalData.MessagingService.Application.Common.Dto;
|
using DigitalData.MessagingService.Application.EmailSending.Commands;
|
||||||
using DigitalData.MessagingService.Client;
|
|
||||||
using DigitalData.MessagingService.Domain.Entities;
|
|
||||||
using EnvelopeGenerator.Application.Common.Interfaces.Services.Notification;
|
using EnvelopeGenerator.Application.Common.Interfaces.Services.Notification;
|
||||||
|
using EnvelopeGenerator.Domain.Constants;
|
||||||
using EnvelopeGenerator.Domain.Entities;
|
using EnvelopeGenerator.Domain.Entities;
|
||||||
using MediatR;
|
using EnvelopeGenerator.Domain.Exceptions;
|
||||||
|
using EnvelopeGenerator.Infrastructure.Notification.Configuration;
|
||||||
using Microsoft.EntityFrameworkCore;
|
using Microsoft.EntityFrameworkCore;
|
||||||
using Microsoft.Extensions.Options;
|
using Microsoft.Extensions.Options;
|
||||||
|
|
||||||
namespace EnvelopeGenerator.Infrastructure.Notification;
|
namespace EnvelopeGenerator.Infrastructure.Notification;
|
||||||
|
|
||||||
public class EnvelopeNotificationService(IOptions<EmailAccount> EmailAccountOptions, IRepository<EmailTemplate> TempRepo) : IEnvelopeNotificationService
|
public class EnvelopeNotificationService(IRepository<EmailTemplate> TempRepo, IMessagingServiceClient MsgClient, IOptions<NotificationOptions> ntfOpt) : IEnvelopeNotificationService
|
||||||
{
|
{
|
||||||
public async Task Notify(string email, DocumentSignedNotification documentSigned, CancellationToken cancel = default)
|
private readonly NotificationOptions NtfOpt = ntfOpt.Value;
|
||||||
{
|
|
||||||
var template = await TempRepo.Where(t => t.Name == "DocumentCompleted").FirstOrDefaultAsync(cancel)
|
|
||||||
?? throw new NotFoundException("DocumentCompleted email template not found");
|
|
||||||
var subjectTemplate = Replace(template.Subject, documentSigned);
|
|
||||||
var bodyTemplate = Replace(template.Body, documentSigned);
|
|
||||||
|
|
||||||
await EmailSender.Send(new EmailContext()
|
private async Task<EmailTemplate> GetTemplateAsync(EmailTemplateType templateType, CancellationToken cancel = default)
|
||||||
{
|
{
|
||||||
Body = bodyTemplate,
|
return await TempRepo.Where(t => t.Name == templateType.ToString()).FirstOrDefaultAsync(cancel)
|
||||||
Subject = subjectTemplate,
|
?? throw new DataIntegrityException($"{templateType} email template not found");
|
||||||
Recipients = [email],
|
|
||||||
Sender = EmailAccountOptions.Value,
|
|
||||||
Attachments = [new EmailAttachmentDto()
|
|
||||||
{
|
|
||||||
FileName = "unterzeichnetes-dokument.pdf",
|
|
||||||
Content = documentSigned.Attachment
|
|
||||||
}]
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private static string Replace(string template, DocumentSignedNotification documentSigned)
|
public async Task Notify<TEnvelopeNotificationBase>(string receiverEmail, TEnvelopeNotificationBase notification, CancellationToken cancel = default)
|
||||||
|
where TEnvelopeNotificationBase : EnvelopeNotificationBase
|
||||||
{
|
{
|
||||||
return template
|
var template = await GetTemplateAsync(notification.GetTemplateType(), cancel);
|
||||||
.Replace("[DOCUMENT_TITLE]", documentSigned.DocumentTitle)
|
|
||||||
.Replace("[NAME_PORTAL]", documentSigned.NamePortal)
|
// create email command
|
||||||
.Replace("[NAME_RECEIVER]", documentSigned.NameReceiver);
|
var mail = new PublishEmailViaSmtpCommand()
|
||||||
|
{
|
||||||
|
Body = template.Body.ReplacePlaceholders(notification),
|
||||||
|
Subject = template.Subject.ReplacePlaceholders(notification),
|
||||||
|
Recipients = [receiverEmail],
|
||||||
|
Sender = NtfOpt.SenderEmail.ToSender(),
|
||||||
|
IsHtml = true
|
||||||
|
};
|
||||||
|
|
||||||
|
// add attachments if any
|
||||||
|
if(notification.Attachments?.Any() ?? false)
|
||||||
|
mail = mail.WithAttachments(notification.Attachments.ToEmailAttachmentDto());
|
||||||
|
|
||||||
|
await MsgClient.SendAsync(mail, cancel);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -0,0 +1,47 @@
|
|||||||
|
using System.Collections.Concurrent;
|
||||||
|
using System.Reflection;
|
||||||
|
using System.Text.RegularExpressions;
|
||||||
|
|
||||||
|
namespace EnvelopeGenerator.Infrastructure.Notification;
|
||||||
|
|
||||||
|
public static class ReplacePlaceholderExtensions
|
||||||
|
{
|
||||||
|
private sealed record PlaceholderProperty(string Placeholder, PropertyInfo Property);
|
||||||
|
|
||||||
|
private static readonly ConcurrentDictionary<Type, PlaceholderProperty[]> PropertyCache = new();
|
||||||
|
|
||||||
|
public static string ReplacePlaceholders(this string template, object data)
|
||||||
|
{
|
||||||
|
var properties = PropertyCache.GetOrAdd(data.GetType(), CreatePlaceholderProperties);
|
||||||
|
|
||||||
|
foreach (var item in properties)
|
||||||
|
{
|
||||||
|
var value = item.Property.GetValue(data)?.ToString() ?? string.Empty;
|
||||||
|
template = template.Replace(
|
||||||
|
item.Placeholder,
|
||||||
|
value,
|
||||||
|
StringComparison.Ordinal);
|
||||||
|
}
|
||||||
|
|
||||||
|
return template;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static PlaceholderProperty[] CreatePlaceholderProperties(
|
||||||
|
Type type)
|
||||||
|
{
|
||||||
|
return [.. type
|
||||||
|
.GetProperties(BindingFlags.Public | BindingFlags.Instance)
|
||||||
|
.Where(p => p.CanRead && p.GetIndexParameters().Length == 0)
|
||||||
|
.Select(p => new PlaceholderProperty(
|
||||||
|
$"[{ToCapitalSnakeCase(p.Name)}]",
|
||||||
|
p))];
|
||||||
|
}
|
||||||
|
|
||||||
|
private static string ToCapitalSnakeCase(string name)
|
||||||
|
{
|
||||||
|
return Regex.Replace(
|
||||||
|
Regex.Replace(name, @"([A-Z]+)([A-Z][a-z])", "$1_$2"),
|
||||||
|
@"([a-z0-9])([A-Z])",
|
||||||
|
"$1_$2").ToUpperInvariant();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,17 @@
|
|||||||
|
-- Original email template: DocumentAccessCodeReceived (GUID 5)
|
||||||
|
-- Backed up before redesign: 2026-10-29 16:32:15
|
||||||
|
-- Subject: Zugriffscode für Dokument erhalten: ''[DOCUMENT_TITLE]''
|
||||||
|
-- Restore by running this script against DD_ECM:
|
||||||
|
UPDATE dbo.TBSIG_EMAIL_TEMPLATE
|
||||||
|
SET BODY = N'Guten Tag [NAME_RECEIVER],<br />
|
||||||
|
<br /><B><I>
|
||||||
|
[NAME_SENDER]</I></B> hat Ihnen ein Dokument zum [SIGNATURE_TYPE] gesendet. <br />
|
||||||
|
<br />
|
||||||
|
Verwenden Sie den folgenden Zugriffscode, um das Dokument einzusehen:<br />
|
||||||
|
<br />
|
||||||
|
[DOCUMENT_ACCESS_CODE]<br />
|
||||||
|
<br />
|
||||||
|
Mit freundlichen Grüßen<br />
|
||||||
|
<br />
|
||||||
|
[NAME_PORTAL]'
|
||||||
|
WHERE GUID = 5;
|
||||||
@@ -0,0 +1,15 @@
|
|||||||
|
-- Original email template: DocumentCompleted (GUID 4)
|
||||||
|
-- Backed up before redesign: 2026-10-29 16:29:55
|
||||||
|
-- Subject: Umschlag abgeschlossen: ''[DOCUMENT_TITLE]''
|
||||||
|
-- Restore by running this script against DD_ECM:
|
||||||
|
UPDATE dbo.TBSIG_EMAIL_TEMPLATE
|
||||||
|
SET BODY = N'Guten Tag [NAME_RECEIVER],<br />
|
||||||
|
<br />
|
||||||
|
Der Vorgang <B><I>''[DOCUMENT_TITLE]''</I></B> wurde erfolgreich abgeschlossen.<br />
|
||||||
|
<br />
|
||||||
|
Sie erhalten das Dokument mit einem detaillierten Ergebnisbericht als Anhang zu dieser Email.<br />
|
||||||
|
<br />
|
||||||
|
Mit freundlichen Grüßen<br />
|
||||||
|
<br />
|
||||||
|
[NAME_PORTAL]'
|
||||||
|
WHERE GUID = 4;
|
||||||
@@ -0,0 +1,13 @@
|
|||||||
|
-- Original email template: DocumentDeleted (GUID 2)
|
||||||
|
-- Backed up before redesign: 2026-10-29 16:23:45
|
||||||
|
-- Subject: Umschlag zurückgezogen: ''[DOCUMENT_TITLE]''
|
||||||
|
-- Restore by running this script against DD_ECM:
|
||||||
|
UPDATE dbo.TBSIG_EMAIL_TEMPLATE
|
||||||
|
SET BODY = N'Guten Tag [NAME_RECEIVER],<br />
|
||||||
|
<br /><B><I>
|
||||||
|
[NAME_SENDER]</I></B> hat den Umschlag <B><I>''[DOCUMENT_TITLE]''</I></B> gelöscht/zurückgezogen.<br /><p>
|
||||||
|
Begründung: <br /> <I>[REASON]</I> <p>
|
||||||
|
<br />
|
||||||
|
Mit freundlichen Grüßen<br />
|
||||||
|
<br />
|
||||||
|
[NAME_PORTAL]'
|
||||||
@@ -0,0 +1,17 @@
|
|||||||
|
-- Original email template: DocumentReceived (GUID 1, de-DE)
|
||||||
|
-- Backed up before redesign: 2026-10-29 16:06:07
|
||||||
|
-- Subject: Dokument erhalten: ''[DOCUMENT_TITLE]''
|
||||||
|
-- Restore by running this script against DD_ECM:
|
||||||
|
UPDATE dbo.TBSIG_EMAIL_TEMPLATE
|
||||||
|
SET BODY = N'Guten Tag [NAME_RECEIVER],<br />
|
||||||
|
<br /><B><I>
|
||||||
|
[NAME_SENDER]</I></B> hat Ihnen ein Dokument zum [SIGNATURE_TYPE] gesendet.<br />
|
||||||
|
<br />
|
||||||
|
Über den folgenden Link können Sie das Dokument einsehen[DOCUMENT_PROCESS]: <a href="[LINK_TO_DOCUMENT]">[LINK_TO_DOCUMENT_TEXT]</a><br />
|
||||||
|
<br />
|
||||||
|
[MESSAGE]<br />
|
||||||
|
<br />
|
||||||
|
Mit freundlichen Grüßen<br />
|
||||||
|
<br />
|
||||||
|
[NAME_PORTAL]'
|
||||||
|
WHERE GUID = 1;
|
||||||
@@ -0,0 +1,12 @@
|
|||||||
|
-- Original email template: DocumentRejected_ADM (GUID 6)
|
||||||
|
-- Backed up before redesign: 2026-10-29 16:33:34
|
||||||
|
-- Subject: ''[DOCUMENT_TITLE]'' - Vorgang zurückgezogen
|
||||||
|
-- Restore by running this script against DD_ECM:
|
||||||
|
UPDATE dbo.TBSIG_EMAIL_TEMPLATE
|
||||||
|
SET BODY = N'Guten Tag [NAME_SENDER],<p><B><I>[NAME_RECEIVER]</I></B> hat den Umschlag <B><I>''[DOCUMENT_TITLE]''</I></B> mit folgendem Grund abgelehnt: <p>
|
||||||
|
[REASON]
|
||||||
|
<p>Der Umschlag wurde auf den Status Rejected gesetzt. <p>
|
||||||
|
Mit freundlichen Grüßen<br />
|
||||||
|
<br />
|
||||||
|
[NAME_PORTAL]'
|
||||||
|
WHERE GUID = 6;
|
||||||
@@ -0,0 +1,11 @@
|
|||||||
|
-- Original email template: DocumentRejected_REC_2 (GUID 10)
|
||||||
|
-- Backed up before redesign: 2026-10-29 16:37:12
|
||||||
|
-- Subject: ''[DOCUMENT_TITLE]'' - Vorgang abgelehnt.
|
||||||
|
-- Restore by running this script against DD_ECM:
|
||||||
|
UPDATE dbo.TBSIG_EMAIL_TEMPLATE
|
||||||
|
SET BODY = N'Guten Tag [NAME_RECEIVER],
|
||||||
|
<p>Der Vorgang <B><I>''[DOCUMENT_TITLE]''</I></B> wurde durch einen anderen Vertragspartner abgelehnt.<p> Der Absender <B><I>[NAME_SENDER]</I></B> wird sich bei Bedarf mit Ihnen in Verbindung setzen. <p>
|
||||||
|
Mit freundlichen Grüßen<br />
|
||||||
|
<br />
|
||||||
|
[NAME_PORTAL]'
|
||||||
|
WHERE GUID = 10;
|
||||||
@@ -0,0 +1,11 @@
|
|||||||
|
-- Original email template: DocumentRejected_REC (GUID 9)
|
||||||
|
-- Backed up before redesign: 2026-10-29 16:34:21
|
||||||
|
-- Subject: ''[DOCUMENT_TITLE]'' - Bestätigung der Ablehnung
|
||||||
|
-- Restore by running this script against DD_ECM:
|
||||||
|
UPDATE dbo.TBSIG_EMAIL_TEMPLATE
|
||||||
|
SET BODY = N'Guten Tag [NAME_RECEIVER],
|
||||||
|
<p>Hiermit bestätigen wir Ihnen die Ablehnung des Vorgangs <B><I>''[DOCUMENT_TITLE]''</I></B>!<p>Der Absender <B><I>[NAME_SENDER]</I></B> wurde über die Ablehnung informiert. <p>
|
||||||
|
Mit freundlichen Grüßen<br />
|
||||||
|
<br />
|
||||||
|
[NAME_PORTAL]'
|
||||||
|
WHERE GUID = 9;
|
||||||
@@ -0,0 +1,7 @@
|
|||||||
|
-- Original email template: DocumentShared (GUID 11)
|
||||||
|
-- Backed up before redesign: 2026-10-29 16:42:51
|
||||||
|
-- Subject: Dokument geteilt: ''[DOCUMENT_TITLE]''
|
||||||
|
-- Restore by running this script against DD_ECM:
|
||||||
|
UPDATE dbo.TBSIG_EMAIL_TEMPLATE
|
||||||
|
SET BODY = N'Guten Tag,<br /> <br /><B><I> [NAME_RECEIVER]</I></B> hat Ihnen ein Dokument zum Ansehen gesendet.<br /> <br /> Über den folgenden Link können Sie das Dokument einsehen: <a href="[LINK_TO_DOCUMENT]">[LINK_TO_DOCUMENT_TEXT]</a><br /> <br /> <br /> Mit freundlichen Grüßen<br /> <br /> [NAME_PORTAL]'
|
||||||
|
WHERE GUID = 11;
|
||||||
@@ -0,0 +1,14 @@
|
|||||||
|
-- Original email template: DocumentSigned (GUID 3)
|
||||||
|
-- Backed up before redesign: 2026-10-29 16:25:45
|
||||||
|
-- Subject: Dokument [RECEIVER_ACTION]: ''[DOCUMENT_TITLE]''
|
||||||
|
-- Restore by running this script against DD_ECM:
|
||||||
|
UPDATE dbo.TBSIG_EMAIL_TEMPLATE
|
||||||
|
SET BODY = N'Guten Tag [NAME_RECEIVER],<br />
|
||||||
|
<br />
|
||||||
|
hiermit bestätigen wir Ihnen die erfolgreiche [FINAL_STATUS] für den Vorgang <B><I>''[DOCUMENT_TITLE]''</I></B>.<br />
|
||||||
|
Wenn alle [FINAL_ACTION] haben, erhalten Sie ebenfalls per E-Mail ein abgeschlossenes Exemplar mit entsprechendem Zertifikat!
|
||||||
|
<br />
|
||||||
|
Mit freundlichen Grüßen<br />
|
||||||
|
<br />
|
||||||
|
[NAME_PORTAL]'
|
||||||
|
WHERE GUID = 3;
|
||||||
@@ -0,0 +1,8 @@
|
|||||||
|
-- Original email template: TotpSecret (GUID 12)
|
||||||
|
-- Backed up before redesign: 2026-10-29 16:44:44
|
||||||
|
-- Subject: 2-Faktor-Authentifizierung QR-Code
|
||||||
|
-- Restore by running this script against DD_ECM:
|
||||||
|
UPDATE dbo.TBSIG_EMAIL_TEMPLATE
|
||||||
|
SET BODY = N'Guten Tag,<br /> <br />Sie können auf Ihren Zwei-Faktor-Authentifizierungscode zugreifen, indem Sie den unten stehenden QR-Code mit einer beliebigen Authentifizierungs-App auf Ihrem Telefon scannen (Google Authenticator, Microsoft Authenticator usw.). Dieser Code ist bis zum [TFA_EXPIRATION] gültig.<br /> <br /> <img src="data:image/png;base64,[TFA_QR_CODE]" style="width: 13rem; height: 13rem;"><br /> <br />
|
||||||
|
<br /> Mit freundlichen Grüßen<br /> <br /> [NAME_PORTAL]'
|
||||||
|
WHERE GUID = 12;
|
||||||
@@ -5,6 +5,7 @@
|
|||||||
@using EnvelopeGenerator.Domain.Resources
|
@using EnvelopeGenerator.Domain.Resources
|
||||||
@using EnvelopeGenerator.Server.Client.Components.Sender.ReceiverSuggestionFilter
|
@using EnvelopeGenerator.Server.Client.Components.Sender.ReceiverSuggestionFilter
|
||||||
@using EnvelopeGenerator.Server.Client.Services
|
@using EnvelopeGenerator.Server.Client.Services
|
||||||
|
@using MudBlazor
|
||||||
@using Microsoft.AspNetCore.Components.Forms
|
@using Microsoft.AspNetCore.Components.Forms
|
||||||
@using Microsoft.Extensions.Localization
|
@using Microsoft.Extensions.Localization
|
||||||
@inject IStringLocalizer<Model> ModelLocalizer
|
@inject IStringLocalizer<Model> ModelLocalizer
|
||||||
@@ -27,334 +28,341 @@
|
|||||||
<div class="sender-create-wizard"
|
<div class="sender-create-wizard"
|
||||||
tabindex="0"
|
tabindex="0"
|
||||||
@onkeydown="OnWizardKeyDownAsync">
|
@onkeydown="OnWizardKeyDownAsync">
|
||||||
<div class="sender-create-wizard__progress">
|
<MudPaper Class="sender-create-wizard__stepper-shell" Elevation="0">
|
||||||
<div class="sender-create-wizard__progress-track"></div>
|
<MudStepper Vertical="true"
|
||||||
@for (var step = 0; step < TotalSteps; step++)
|
NonLinear="false"
|
||||||
{
|
ShowResetButton="false"
|
||||||
var isActive = step == _currentStep;
|
ActiveIndex="@_currentStep"
|
||||||
var isDone = step < _currentStep;
|
Class="sender-create-wizard__stepper">
|
||||||
<div class="sender-create-wizard__step @(isActive ? "sender-create-wizard__step--active" : string.Empty) @(isDone ? "sender-create-wizard__step--done" : string.Empty)">
|
<ChildContent>
|
||||||
<span class="sender-create-wizard__step-index">@(step + 1)</span>
|
@for (var step = 0; step < TotalSteps; step++)
|
||||||
</div>
|
{
|
||||||
}
|
var stepIndex = step;
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="sender-create-wizard__meta">
|
<MudStep Title="@GetStepTitle(stepIndex)">
|
||||||
<div class="sender-create-wizard__title">@CurrentStepTitle</div>
|
<div class="sender-create-wizard__stepper-description">@GetStepDescription(stepIndex)</div>
|
||||||
<div class="sender-create-wizard__description">@CurrentStepDescription</div>
|
|
||||||
</div>
|
@if (stepIndex == _currentStep)
|
||||||
|
{
|
||||||
|
<div class="sender-create-wizard__step-content">
|
||||||
|
@if (_currentStep == 0)
|
||||||
|
{
|
||||||
|
<div class="sender-create-wizard__form-grid">
|
||||||
|
<div class="sender-create-wizard__field">
|
||||||
|
<label class="sender-create-wizard__label">
|
||||||
|
@L("Title", "Titel") <span class="sender-create-wizard__required">*</span>
|
||||||
|
</label>
|
||||||
|
<DxTextBox Text="@_title"
|
||||||
|
TextChanged="OnTitleChanged"
|
||||||
|
BindValueMode="BindValueMode.OnInput"
|
||||||
|
NullText='@L("SenderEditor_Title_Placeholder", "z.B. Arbeitsvertrag 2025")'
|
||||||
|
CssClass="w-100" />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="sender-create-wizard__field">
|
||||||
|
<label class="sender-create-wizard__label">
|
||||||
|
@L("EnvelopeType", "Umschlagtyp") <span class="sender-create-wizard__required">*</span>
|
||||||
|
</label>
|
||||||
|
<select value="@(_selectedEnvelopeTypeId?.ToString() ?? string.Empty)"
|
||||||
|
@onchange="OnEnvelopeTypeChanged"
|
||||||
|
class="sender-create-wizard__select">
|
||||||
|
@foreach (var type in EnvelopeTypes)
|
||||||
|
{
|
||||||
|
<option value="@type.Id">@type.Title</option>
|
||||||
|
}
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
else if (_currentStep == 3)
|
||||||
|
{
|
||||||
|
<div class="sender-create-wizard__form-grid">
|
||||||
|
<div class="sender-create-wizard__field sender-create-wizard__field--full">
|
||||||
|
<label class="sender-create-wizard__label">
|
||||||
|
@L("Message", "Nachricht") <span class="sender-create-wizard__optional">(@L("Optional", "optional"))</span>
|
||||||
|
</label>
|
||||||
|
<DxMemo Text="@_message"
|
||||||
|
TextChanged="OnMessageChanged"
|
||||||
|
Rows="5"
|
||||||
|
MaxLength="500"
|
||||||
|
NullText='@L("SenderEditor_Message_Placeholder", "Bitte unterzeichnen Sie das beigefügte Dokument.")'
|
||||||
|
CssClass="w-100" />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="sender-create-wizard__field">
|
||||||
|
<label class="sender-create-wizard__label">
|
||||||
|
@L("Language", "Sprache")
|
||||||
|
</label>
|
||||||
|
<select @bind="_language" class="sender-create-wizard__select">
|
||||||
|
<option value="de-DE">Deutsch (de-DE)</option>
|
||||||
|
<option value="en-US">English (en-US)</option>
|
||||||
|
<option value="de">Deutsch (de)</option>
|
||||||
|
<option value="en">English (en)</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
else if (_currentStep == 2)
|
||||||
|
{
|
||||||
|
<div class="sender-create-wizard__settings-grid">
|
||||||
|
<div class="sender-create-wizard__settings-group">
|
||||||
|
<div class="sender-create-wizard__settings-title">
|
||||||
|
@L("SenderEditor_EnvelopeSettings", "Envelope-Einstellungen")
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="sender-create-wizard__form-grid">
|
||||||
|
<div class="sender-create-wizard__field">
|
||||||
|
<label class="sender-create-wizard__label">@L("CertificationType", "Zertifizierung")</label>
|
||||||
|
<select @bind="_certificationType" class="sender-create-wizard__select">
|
||||||
|
<option value="@CertificationType.AdvancedElectronicSignature">@L("AdvancedElectronicSignature", "Advanced Electronic Signature")</option>
|
||||||
|
<option value="@CertificationType.ElectronicSignature">@L("ElectronicSignature", "Electronic Signature")</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="sender-create-wizard__field">
|
||||||
|
<label class="sender-create-wizard__label">@L("FinalEmailToCreator", "Finale E-Mail an Ersteller")</label>
|
||||||
|
<select @bind="_finalEmailToCreator" class="sender-create-wizard__select">
|
||||||
|
<option value="0">@L("No", "Nein")</option>
|
||||||
|
<option value="1">@L("Yes", "Ja")</option>
|
||||||
|
<option value="2">@L("YesWithAttachment", "Ja, mit Anhang")</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="sender-create-wizard__field">
|
||||||
|
<label class="sender-create-wizard__label">@L("FinalEmailToReceivers", "Finale E-Mail an Empfänger")</label>
|
||||||
|
<select @bind="_finalEmailToReceivers" class="sender-create-wizard__select">
|
||||||
|
<option value="0">@L("No", "Nein")</option>
|
||||||
|
<option value="1">@L("Yes", "Ja")</option>
|
||||||
|
<option value="2">@L("YesWithAttachment", "Ja, mit Anhang")</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="sender-create-wizard__field sender-create-wizard__field--full">
|
||||||
|
<div class="sender-create-wizard__settings-toggles">
|
||||||
|
<label class="sender-create-wizard__settings-toggle">
|
||||||
|
<input type="checkbox" checked="@_tfaEnabled" @onchange="OnTfaChanged" />
|
||||||
|
@L("TFAEnabled", "2FA aktiviert")
|
||||||
|
</label>
|
||||||
|
<label class="sender-create-wizard__settings-toggle">
|
||||||
|
<input type="checkbox" checked="@_useAccessCode" @onchange="OnUseAccessCodeChanged" disabled="@_tfaEnabled" />
|
||||||
|
@L("UseAccessCode", "Zugriffscode verwenden")
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="sender-create-wizard__settings-group">
|
||||||
|
<div class="sender-create-wizard__settings-title">
|
||||||
|
@L("SendReminderEmails", "Erinnerungs-E-Mails senden")
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="sender-create-wizard__form-grid">
|
||||||
|
<div class="sender-create-wizard__field sender-create-wizard__field--full">
|
||||||
|
<label class="sender-create-wizard__settings-toggle">
|
||||||
|
<input type="checkbox" checked="@_sendReminderEmails" @onchange="OnSendReminderEmailsChanged" />
|
||||||
|
@L("SendReminderEmails", "Erinnerungs-E-Mails senden")
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="sender-create-wizard__field">
|
||||||
|
<label class="sender-create-wizard__label">@L("FirstReminderDays", "Erste Erinnerung (Tage)")</label>
|
||||||
|
<InputNumber @bind-Value="_firstReminderDays" class="form-control form-control-sm" disabled="@(!_sendReminderEmails)" />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="sender-create-wizard__field">
|
||||||
|
<label class="sender-create-wizard__label">@L("ReminderIntervalDays", "Erinnerungsintervall (Tage)")</label>
|
||||||
|
<InputNumber @bind-Value="_reminderIntervalDays" class="form-control form-control-sm" disabled="@(!_sendReminderEmails)" />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="sender-create-wizard__field">
|
||||||
|
<label class="sender-create-wizard__label">@L("ExpiresWhenDays", "Läuft ab nach (Tage)")</label>
|
||||||
|
<InputNumber @bind-Value="_expiresWhenDays" class="form-control form-control-sm" />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="sender-create-wizard__field">
|
||||||
|
<label class="sender-create-wizard__label">@L("ExpiresWarningWhenDays", "Ablaufwarnung (Tage)")</label>
|
||||||
|
<InputNumber @bind-Value="_expiresWarningWhenDays" class="form-control form-control-sm" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
else if (_currentStep == 1)
|
||||||
|
{
|
||||||
|
<div class="sender-create-wizard__suggestions">
|
||||||
|
<div class="sender-create-wizard__suggestions-title">@L("SenderWizard_RecentReceivers", "Zuletzt verwendete Empfänger")</div>
|
||||||
|
|
||||||
|
@if (_isLoadingRecentSuggestions)
|
||||||
|
{
|
||||||
|
<div class="sender-create-wizard__empty-list">@L("Loading", "Lädt...")</div>
|
||||||
|
}
|
||||||
|
else if (_recentReceiverSuggestions.Count > 0)
|
||||||
|
{
|
||||||
|
<div class="sender-create-wizard__suggestion-list" role="listbox" aria-label="Recent receivers">
|
||||||
|
@foreach (var suggestion in FilteredRecentSuggestions)
|
||||||
|
{
|
||||||
|
<button type="button"
|
||||||
|
class="sender-create-wizard__suggestion-item"
|
||||||
|
@onclick="() => ApplyRecentSuggestion(suggestion)"
|
||||||
|
disabled="@IsActionLocked">
|
||||||
|
<span class="sender-create-wizard__suggestion-name">@suggestion.FullName</span>
|
||||||
|
<span class="sender-create-wizard__suggestion-email">@suggestion.EmailAddress</span>
|
||||||
|
</button>
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="sender-create-wizard__form-grid">
|
||||||
|
<div class="sender-create-wizard__field">
|
||||||
|
<label class="sender-create-wizard__label">
|
||||||
|
@L("FullName", "Vor- und Nachname") <span class="sender-create-wizard__required">*</span>
|
||||||
|
</label>
|
||||||
|
<DxTextBox Text="@_receiverName"
|
||||||
|
TextChanged="OnReceiverNameChanged"
|
||||||
|
BindValueMode="BindValueMode.OnInput"
|
||||||
|
NullText="Max Mustermann"
|
||||||
|
CssClass="w-100" />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="sender-create-wizard__field">
|
||||||
|
<label class="sender-create-wizard__label">
|
||||||
|
@L("EmailAddress", "E-Mail-Adresse") <span class="sender-create-wizard__required">*</span>
|
||||||
|
</label>
|
||||||
|
<DxTextBox Text="@_receiverEmail"
|
||||||
|
TextChanged="OnReceiverEmailChanged"
|
||||||
|
BindValueMode="BindValueMode.OnInput"
|
||||||
|
NullText="name@beispiel.de"
|
||||||
|
CssClass="w-100" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="sender-create-wizard__receiver-actions">
|
||||||
|
<button type="button"
|
||||||
|
class="sender-create-wizard__add-receiver-btn"
|
||||||
|
@onclick="AddReceiver"
|
||||||
|
disabled="@IsActionLocked">
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" width="14" height="14" fill="currentColor" viewBox="0 0 16 16" aria-hidden="true">
|
||||||
|
<path d="M8 4a.5.5 0 0 1 .5.5V7.5H11.5a.5.5 0 0 1 0 1H8.5v3a.5.5 0 0 1-1 0v-3H4.5a.5.5 0 0 1 0-1H7.5v-3A.5.5 0 0 1 8 4z" />
|
||||||
|
</svg>
|
||||||
|
<span>@L("AddReceiver", "Empfänger hinzufügen")</span>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
@if (_receivers.Count == 0)
|
||||||
|
{
|
||||||
|
<div class="sender-create-wizard__empty-list">
|
||||||
|
@L("SenderEditor_NoReceiversAdded", "Noch keine Empfänger hinzugefügt.")
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
<div class="sender-create-wizard__receiver-tagbox-wrap">
|
||||||
|
<label class="sender-create-wizard__label" for="wizardReceiversTagBox">
|
||||||
|
@L("Receiver", "Empfänger")
|
||||||
|
</label>
|
||||||
|
<DxTagBox Data="@_receivers"
|
||||||
|
TData="CreateEnvelopeWizardReceiver"
|
||||||
|
TValue="CreateEnvelopeWizardReceiver"
|
||||||
|
AllowCustomTags="true"
|
||||||
|
@bind-Tags="@_receiverTags"
|
||||||
|
@bind-Tags:after="OnReceiverTagsChanged"
|
||||||
|
HideSelectedItems="false"
|
||||||
|
TextFieldName="@nameof(CreateEnvelopeWizardReceiver.EmailAddress)"
|
||||||
|
ListRenderMode="ListRenderMode.Virtual"
|
||||||
|
SearchMode="ListSearchMode.AutoSearch"
|
||||||
|
SearchFilterCondition="ListSearchFilterCondition.Contains"
|
||||||
|
SearchTextParseMode="ListSearchTextParseMode.GroupWordsByAnd"
|
||||||
|
CssClass="sender-create-wizard__receiver-tagbox"
|
||||||
|
InputId="wizardReceiversTagBox"
|
||||||
|
Enabled="@(!IsActionLocked)">
|
||||||
|
<TagDisplayTemplate Context="tagContext">
|
||||||
|
<div class="sender-create-wizard__receiver-tag">
|
||||||
|
@if (tagContext.DataItem is CreateEnvelopeWizardReceiver receiver)
|
||||||
|
{
|
||||||
|
<span class="sender-create-wizard__receiver-tag-name">@receiver.FullName</span>
|
||||||
|
<span class="sender-create-wizard__receiver-tag-email">@receiver.EmailAddress</span>
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
<span class="sender-create-wizard__receiver-tag-email">@tagContext.DisplayText</span>
|
||||||
|
}
|
||||||
|
<button type="button"
|
||||||
|
class="sender-create-wizard__receiver-tag-remove"
|
||||||
|
@onclick="tagContext.RemoveTagAction"
|
||||||
|
aria-label="Remove receiver"
|
||||||
|
disabled="@IsActionLocked">x</button>
|
||||||
|
</div>
|
||||||
|
</TagDisplayTemplate>
|
||||||
|
<EmptyDataAreaTemplate Context="emptyContext">
|
||||||
|
<div class="sender-create-wizard__tagbox-empty"></div>
|
||||||
|
</EmptyDataAreaTemplate>
|
||||||
|
</DxTagBox>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
<div class="sender-create-wizard__upload-stage">
|
||||||
|
<label class="sender-create-wizard__upload-btn @(IsActionLocked ? "sender-create-wizard__upload-btn--disabled" : string.Empty)">
|
||||||
|
@if (_documentBytes is null)
|
||||||
|
{
|
||||||
|
<span>@L("UploadPdf", "PDF hochladen")</span>
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
<span>@L("SenderWizard_ReplacePdf", "PDF ersetzen")</span>
|
||||||
|
}
|
||||||
|
<InputFile OnChange="OnDocumentSelectedAsync"
|
||||||
|
accept=".pdf"
|
||||||
|
disabled="@IsActionLocked"
|
||||||
|
class="sender-create-wizard__hidden-input" />
|
||||||
|
</label>
|
||||||
|
|
||||||
|
@if (_documentBytes is not null)
|
||||||
|
{
|
||||||
|
<div class="sender-create-wizard__document-preview-card">
|
||||||
|
<div class="sender-create-wizard__document-meta">
|
||||||
|
<div class="sender-create-wizard__document-name">@_documentFileName</div>
|
||||||
|
<div class="sender-create-wizard__document-size">@_documentSizeText</div>
|
||||||
|
</div>
|
||||||
|
<div class="sender-create-wizard__document-preview-wrap">
|
||||||
|
<SenderPdfImageEditor IsPlacementMode="false"
|
||||||
|
PageCount="@_documentPreviewPageCount"
|
||||||
|
Pages="@_documentPreviewPages"
|
||||||
|
PdfDataUrl="@DocumentPreviewDataUrl"
|
||||||
|
PdfFileName="@_documentFileName"
|
||||||
|
Fields="@_documentPreviewFieldOverlays"
|
||||||
|
EditorClick="OnDocumentPreviewEditorClickAsync"
|
||||||
|
RemoveField="OnDocumentPreviewRemoveFieldAsync"
|
||||||
|
MoveField="OnDocumentPreviewMoveFieldAsync" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
<PdfFileDropZone Disabled="@IsActionLocked"
|
||||||
|
HasDocument="false"
|
||||||
|
UploadText='@L("SenderWizard_DocumentRequiredHint", "Bitte laden Sie ein PDF-Dokument hoch, um fortzufahren.")'
|
||||||
|
DragDropHintText='@L("SenderWizard_DragDropPdfHint", "Oder ziehen Sie die PDF-Datei hierher.")'
|
||||||
|
FileSelected="OnDocumentSelectedAsync" />
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
</MudStep>
|
||||||
|
}
|
||||||
|
</ChildContent>
|
||||||
|
</MudStepper>
|
||||||
|
</MudPaper>
|
||||||
|
|
||||||
@if (!string.IsNullOrWhiteSpace(_validationMessage))
|
@if (!string.IsNullOrWhiteSpace(_validationMessage))
|
||||||
{
|
{
|
||||||
<div class="sender-create-wizard__validation">@_validationMessage</div>
|
<div class="sender-create-wizard__validation">@_validationMessage</div>
|
||||||
}
|
}
|
||||||
|
|
||||||
@if (_currentStep == 0)
|
|
||||||
{
|
|
||||||
<div class="sender-create-wizard__form-grid">
|
|
||||||
<div class="sender-create-wizard__field">
|
|
||||||
<label class="sender-create-wizard__label">
|
|
||||||
@L("Title", "Titel") <span class="sender-create-wizard__required">*</span>
|
|
||||||
</label>
|
|
||||||
<DxTextBox Text="@_title"
|
|
||||||
TextChanged="OnTitleChanged"
|
|
||||||
BindValueMode="BindValueMode.OnInput"
|
|
||||||
NullText='@L("SenderEditor_Title_Placeholder", "z.B. Arbeitsvertrag 2025")'
|
|
||||||
CssClass="w-100" />
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="sender-create-wizard__field">
|
|
||||||
<label class="sender-create-wizard__label">
|
|
||||||
@L("EnvelopeType", "Umschlagtyp") <span class="sender-create-wizard__required">*</span>
|
|
||||||
</label>
|
|
||||||
<select value="@(_selectedEnvelopeTypeId?.ToString() ?? string.Empty)"
|
|
||||||
@onchange="OnEnvelopeTypeChanged"
|
|
||||||
class="sender-create-wizard__select">
|
|
||||||
@foreach (var type in EnvelopeTypes)
|
|
||||||
{
|
|
||||||
<option value="@type.Id">@type.Title</option>
|
|
||||||
}
|
|
||||||
</select>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
}
|
|
||||||
else if (_currentStep == 1)
|
|
||||||
{
|
|
||||||
<div class="sender-create-wizard__form-grid">
|
|
||||||
<div class="sender-create-wizard__field sender-create-wizard__field--full">
|
|
||||||
<label class="sender-create-wizard__label">
|
|
||||||
@L("Message", "Nachricht") <span class="sender-create-wizard__optional">(@L("Optional", "optional"))</span>
|
|
||||||
</label>
|
|
||||||
<DxMemo Text="@_message"
|
|
||||||
TextChanged="OnMessageChanged"
|
|
||||||
Rows="5"
|
|
||||||
MaxLength="500"
|
|
||||||
NullText='@L("SenderEditor_Message_Placeholder", "Bitte unterzeichnen Sie das beigefügte Dokument.")'
|
|
||||||
CssClass="w-100" />
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="sender-create-wizard__field">
|
|
||||||
<label class="sender-create-wizard__label">
|
|
||||||
@L("Language", "Sprache")
|
|
||||||
</label>
|
|
||||||
<select @bind="_language" class="sender-create-wizard__select">
|
|
||||||
<option value="de-DE">Deutsch (de-DE)</option>
|
|
||||||
<option value="en-US">English (en-US)</option>
|
|
||||||
<option value="de">Deutsch (de)</option>
|
|
||||||
<option value="en">English (en)</option>
|
|
||||||
</select>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
}
|
|
||||||
else if (_currentStep == 2)
|
|
||||||
{
|
|
||||||
<div class="sender-create-wizard__settings-grid">
|
|
||||||
<div class="sender-create-wizard__settings-group">
|
|
||||||
<div class="sender-create-wizard__settings-title">
|
|
||||||
@L("SenderEditor_EnvelopeSettings", "Envelope-Einstellungen")
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="sender-create-wizard__form-grid">
|
|
||||||
<div class="sender-create-wizard__field">
|
|
||||||
<label class="sender-create-wizard__label">@L("CertificationType", "Zertifizierung")</label>
|
|
||||||
<select @bind="_certificationType" class="sender-create-wizard__select">
|
|
||||||
<option value="1">@L("AdvancedElectronicSignature", "Advanced Electronic Signature")</option>
|
|
||||||
<option value="2">@L("ElectronicSignature", "Electronic Signature")</option>
|
|
||||||
<option value="3">@L("QualifiedSignature", "Qualified Signature")</option>
|
|
||||||
</select>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="sender-create-wizard__field">
|
|
||||||
<label class="sender-create-wizard__label">@L("FinalEmailToCreator", "Finale E-Mail an Ersteller")</label>
|
|
||||||
<select @bind="_finalEmailToCreator" class="sender-create-wizard__select">
|
|
||||||
<option value="0">@L("No", "Nein")</option>
|
|
||||||
<option value="1">@L("Yes", "Ja")</option>
|
|
||||||
<option value="2">@L("YesWithAttachment", "Ja, mit Anhang")</option>
|
|
||||||
</select>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="sender-create-wizard__field">
|
|
||||||
<label class="sender-create-wizard__label">@L("FinalEmailToReceivers", "Finale E-Mail an Empfänger")</label>
|
|
||||||
<select @bind="_finalEmailToReceivers" class="sender-create-wizard__select">
|
|
||||||
<option value="0">@L("No", "Nein")</option>
|
|
||||||
<option value="1">@L("Yes", "Ja")</option>
|
|
||||||
<option value="2">@L("YesWithAttachment", "Ja, mit Anhang")</option>
|
|
||||||
</select>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="sender-create-wizard__field sender-create-wizard__field--full">
|
|
||||||
<div class="sender-create-wizard__settings-toggles">
|
|
||||||
<label class="sender-create-wizard__settings-toggle">
|
|
||||||
<input type="checkbox" checked="@_tfaEnabled" @onchange="OnTfaChanged" />
|
|
||||||
@L("TFAEnabled", "2FA aktiviert")
|
|
||||||
</label>
|
|
||||||
<label class="sender-create-wizard__settings-toggle">
|
|
||||||
<input type="checkbox" checked="@_useAccessCode" @onchange="OnUseAccessCodeChanged" disabled="@_tfaEnabled" />
|
|
||||||
@L("UseAccessCode", "Zugriffscode verwenden")
|
|
||||||
</label>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="sender-create-wizard__settings-group">
|
|
||||||
<div class="sender-create-wizard__settings-title">
|
|
||||||
@L("SendReminderEmails", "Erinnerungs-E-Mails senden")
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="sender-create-wizard__form-grid">
|
|
||||||
<div class="sender-create-wizard__field sender-create-wizard__field--full">
|
|
||||||
<label class="sender-create-wizard__settings-toggle">
|
|
||||||
<input type="checkbox" checked="@_sendReminderEmails" @onchange="OnSendReminderEmailsChanged" />
|
|
||||||
@L("SendReminderEmails", "Erinnerungs-E-Mails senden")
|
|
||||||
</label>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="sender-create-wizard__field">
|
|
||||||
<label class="sender-create-wizard__label">@L("FirstReminderDays", "Erste Erinnerung (Tage)")</label>
|
|
||||||
<InputNumber @bind-Value="_firstReminderDays" class="form-control form-control-sm" disabled="@(!_sendReminderEmails)" />
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="sender-create-wizard__field">
|
|
||||||
<label class="sender-create-wizard__label">@L("ReminderIntervalDays", "Erinnerungsintervall (Tage)")</label>
|
|
||||||
<InputNumber @bind-Value="_reminderIntervalDays" class="form-control form-control-sm" disabled="@(!_sendReminderEmails)" />
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="sender-create-wizard__field">
|
|
||||||
<label class="sender-create-wizard__label">@L("ExpiresWhenDays", "Läuft ab nach (Tage)")</label>
|
|
||||||
<InputNumber @bind-Value="_expiresWhenDays" class="form-control form-control-sm" />
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="sender-create-wizard__field">
|
|
||||||
<label class="sender-create-wizard__label">@L("ExpiresWarningWhenDays", "Ablaufwarnung (Tage)")</label>
|
|
||||||
<InputNumber @bind-Value="_expiresWarningWhenDays" class="form-control form-control-sm" />
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
}
|
|
||||||
else if (_currentStep == 3)
|
|
||||||
{
|
|
||||||
<div class="sender-create-wizard__suggestions">
|
|
||||||
<div class="sender-create-wizard__suggestions-title">@L("SenderWizard_RecentReceivers", "Zuletzt verwendete Empfänger")</div>
|
|
||||||
|
|
||||||
@if (_isLoadingRecentSuggestions)
|
|
||||||
{
|
|
||||||
<div class="sender-create-wizard__empty-list">@L("Loading", "Lädt...")</div>
|
|
||||||
}
|
|
||||||
else if (_recentReceiverSuggestions.Count > 0)
|
|
||||||
{
|
|
||||||
<div class="sender-create-wizard__suggestion-list" role="listbox" aria-label="Recent receivers">
|
|
||||||
@foreach (var suggestion in FilteredRecentSuggestions)
|
|
||||||
{
|
|
||||||
<button type="button"
|
|
||||||
class="sender-create-wizard__suggestion-item"
|
|
||||||
@onclick="() => ApplyRecentSuggestion(suggestion)"
|
|
||||||
disabled="@IsActionLocked">
|
|
||||||
<span class="sender-create-wizard__suggestion-name">@suggestion.FullName</span>
|
|
||||||
<span class="sender-create-wizard__suggestion-email">@suggestion.EmailAddress</span>
|
|
||||||
</button>
|
|
||||||
}
|
|
||||||
</div>
|
|
||||||
}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="sender-create-wizard__form-grid">
|
|
||||||
<div class="sender-create-wizard__field">
|
|
||||||
<label class="sender-create-wizard__label">
|
|
||||||
@L("FullName", "Vor- und Nachname") <span class="sender-create-wizard__required">*</span>
|
|
||||||
</label>
|
|
||||||
<DxTextBox Text="@_receiverName"
|
|
||||||
TextChanged="OnReceiverNameChanged"
|
|
||||||
BindValueMode="BindValueMode.OnInput"
|
|
||||||
NullText="Max Mustermann"
|
|
||||||
CssClass="w-100" />
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="sender-create-wizard__field">
|
|
||||||
<label class="sender-create-wizard__label">
|
|
||||||
@L("EmailAddress", "E-Mail-Adresse") <span class="sender-create-wizard__required">*</span>
|
|
||||||
</label>
|
|
||||||
<DxTextBox Text="@_receiverEmail"
|
|
||||||
TextChanged="OnReceiverEmailChanged"
|
|
||||||
BindValueMode="BindValueMode.OnInput"
|
|
||||||
NullText="name@beispiel.de"
|
|
||||||
CssClass="w-100" />
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="sender-create-wizard__receiver-actions">
|
|
||||||
<button type="button"
|
|
||||||
class="sender-create-wizard__add-receiver-btn"
|
|
||||||
@onclick="AddReceiver"
|
|
||||||
disabled="@IsActionLocked">
|
|
||||||
<svg xmlns="http://www.w3.org/2000/svg" width="14" height="14" fill="currentColor" viewBox="0 0 16 16" aria-hidden="true">
|
|
||||||
<path d="M8 4a.5.5 0 0 1 .5.5V7.5H11.5a.5.5 0 0 1 0 1H8.5v3a.5.5 0 0 1-1 0v-3H4.5a.5.5 0 0 1 0-1H7.5v-3A.5.5 0 0 1 8 4z" />
|
|
||||||
</svg>
|
|
||||||
<span>@L("AddReceiver", "Empfänger hinzufügen")</span>
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
@if (_receivers.Count == 0)
|
|
||||||
{
|
|
||||||
<div class="sender-create-wizard__empty-list">
|
|
||||||
@L("SenderEditor_NoReceiversAdded", "Noch keine Empfänger hinzugefügt.")
|
|
||||||
</div>
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
<div class="sender-create-wizard__receiver-tagbox-wrap">
|
|
||||||
<label class="sender-create-wizard__label" for="wizardReceiversTagBox">
|
|
||||||
@L("Receiver", "Empfänger")
|
|
||||||
</label>
|
|
||||||
<DxTagBox Data="@_receivers"
|
|
||||||
TData="CreateEnvelopeWizardReceiver"
|
|
||||||
TValue="CreateEnvelopeWizardReceiver"
|
|
||||||
AllowCustomTags="true"
|
|
||||||
@bind-Tags="@_receiverTags"
|
|
||||||
@bind-Tags:after="OnReceiverTagsChanged"
|
|
||||||
HideSelectedItems="false"
|
|
||||||
TextFieldName="@nameof(CreateEnvelopeWizardReceiver.EmailAddress)"
|
|
||||||
ListRenderMode="ListRenderMode.Virtual"
|
|
||||||
SearchMode="ListSearchMode.AutoSearch"
|
|
||||||
SearchFilterCondition="ListSearchFilterCondition.Contains"
|
|
||||||
SearchTextParseMode="ListSearchTextParseMode.GroupWordsByAnd"
|
|
||||||
CssClass="sender-create-wizard__receiver-tagbox"
|
|
||||||
InputId="wizardReceiversTagBox"
|
|
||||||
Enabled="@(!IsActionLocked)">
|
|
||||||
<TagDisplayTemplate Context="tagContext">
|
|
||||||
<div class="sender-create-wizard__receiver-tag">
|
|
||||||
@if (tagContext.DataItem is CreateEnvelopeWizardReceiver receiver)
|
|
||||||
{
|
|
||||||
<span class="sender-create-wizard__receiver-tag-name">@receiver.FullName</span>
|
|
||||||
<span class="sender-create-wizard__receiver-tag-email">@receiver.EmailAddress</span>
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
<span class="sender-create-wizard__receiver-tag-email">@tagContext.DisplayText</span>
|
|
||||||
}
|
|
||||||
<button type="button"
|
|
||||||
class="sender-create-wizard__receiver-tag-remove"
|
|
||||||
@onclick="tagContext.RemoveTagAction"
|
|
||||||
aria-label="Remove receiver"
|
|
||||||
disabled="@IsActionLocked">x</button>
|
|
||||||
</div>
|
|
||||||
</TagDisplayTemplate>
|
|
||||||
<EmptyDataAreaTemplate Context="emptyContext">
|
|
||||||
<div class="sender-create-wizard__tagbox-empty"></div>
|
|
||||||
</EmptyDataAreaTemplate>
|
|
||||||
</DxTagBox>
|
|
||||||
</div>
|
|
||||||
}
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
<div class="sender-create-wizard__upload-stage">
|
|
||||||
<label class="sender-create-wizard__upload-btn @(IsActionLocked ? "sender-create-wizard__upload-btn--disabled" : string.Empty)">
|
|
||||||
@if (_documentBytes is null)
|
|
||||||
{
|
|
||||||
<span>@L("UploadPdf", "PDF hochladen")</span>
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
<span>@L("SenderWizard_ReplacePdf", "PDF ersetzen")</span>
|
|
||||||
}
|
|
||||||
<InputFile OnChange="OnDocumentSelectedAsync"
|
|
||||||
accept=".pdf"
|
|
||||||
disabled="@IsActionLocked"
|
|
||||||
class="sender-create-wizard__hidden-input" />
|
|
||||||
</label>
|
|
||||||
|
|
||||||
@if (_documentBytes is not null)
|
|
||||||
{
|
|
||||||
<div class="sender-create-wizard__document-preview-card">
|
|
||||||
<div class="sender-create-wizard__document-meta">
|
|
||||||
<div class="sender-create-wizard__document-name">@_documentFileName</div>
|
|
||||||
<div class="sender-create-wizard__document-size">@_documentSizeText</div>
|
|
||||||
</div>
|
|
||||||
<div class="sender-create-wizard__document-preview-wrap">
|
|
||||||
<SenderPdfImageEditor IsPlacementMode="false"
|
|
||||||
PageCount="@_documentPreviewPageCount"
|
|
||||||
Pages="@_documentPreviewPages"
|
|
||||||
PdfDataUrl="@DocumentPreviewDataUrl"
|
|
||||||
PdfFileName="@_documentFileName"
|
|
||||||
Fields="@_documentPreviewFieldOverlays"
|
|
||||||
EditorClick="OnDocumentPreviewEditorClickAsync"
|
|
||||||
RemoveField="OnDocumentPreviewRemoveFieldAsync"
|
|
||||||
MoveField="OnDocumentPreviewMoveFieldAsync" />
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
<PdfFileDropZone Disabled="@IsActionLocked"
|
|
||||||
HasDocument="false"
|
|
||||||
UploadText='@L("SenderWizard_DocumentRequiredHint", "Bitte laden Sie ein PDF-Dokument hoch, um fortzufahren.")'
|
|
||||||
DragDropHintText='@L("SenderWizard_DragDropPdfHint", "Oder ziehen Sie die PDF-Datei hierher.")'
|
|
||||||
FileSelected="OnDocumentSelectedAsync" />
|
|
||||||
}
|
|
||||||
</div>
|
|
||||||
}
|
|
||||||
</div>
|
</div>
|
||||||
</BodyContentTemplate>
|
</BodyContentTemplate>
|
||||||
<FooterContentTemplate>
|
<FooterContentTemplate>
|
||||||
@@ -421,7 +429,7 @@
|
|||||||
private string _language = "de-DE";
|
private string _language = "de-DE";
|
||||||
private bool _useAccessCode;
|
private bool _useAccessCode;
|
||||||
private bool _tfaEnabled;
|
private bool _tfaEnabled;
|
||||||
private int _certificationType = (int)CertificationType.AdvancedElectronicSignature;
|
private CertificationType _certificationType = CertificationType.AdvancedElectronicSignature;
|
||||||
private int _finalEmailToCreator = (int)FinalEmailType.No;
|
private int _finalEmailToCreator = (int)FinalEmailType.No;
|
||||||
private int _finalEmailToReceivers = (int)FinalEmailType.No;
|
private int _finalEmailToReceivers = (int)FinalEmailType.No;
|
||||||
private bool _sendReminderEmails;
|
private bool _sendReminderEmails;
|
||||||
@@ -457,7 +465,7 @@
|
|||||||
|
|
||||||
private bool IsFirstStep => _currentStep <= 0;
|
private bool IsFirstStep => _currentStep <= 0;
|
||||||
private bool IsLastStep => _currentStep >= TotalSteps - 1;
|
private bool IsLastStep => _currentStep >= TotalSteps - 1;
|
||||||
private bool CanSkipCurrentStep => _currentStep == 1;
|
private bool CanSkipCurrentStep => _currentStep == 3;
|
||||||
|
|
||||||
private IReadOnlyList<RecentReceiverSuggestionDto> FilteredRecentSuggestions =>
|
private IReadOnlyList<RecentReceiverSuggestionDto> FilteredRecentSuggestions =>
|
||||||
BuildFilteredRecentSuggestions();
|
BuildFilteredRecentSuggestions();
|
||||||
@@ -466,22 +474,26 @@
|
|||||||
? $"data:application/pdf;base64,{Convert.ToBase64String(_documentBytes)}"
|
? $"data:application/pdf;base64,{Convert.ToBase64String(_documentBytes)}"
|
||||||
: null;
|
: null;
|
||||||
|
|
||||||
private string CurrentStepTitle => _currentStep switch
|
private string CurrentStepTitle => GetStepTitle(_currentStep);
|
||||||
|
|
||||||
|
private string CurrentStepDescription => GetStepDescription(_currentStep);
|
||||||
|
|
||||||
|
private string GetStepTitle(int step) => step switch
|
||||||
{
|
{
|
||||||
0 => L("SenderWizard_Step_Basics", "Grunddaten"),
|
0 => L("SenderWizard_Step_Basics", "Grunddaten"),
|
||||||
1 => L("SenderWizard_Step_Message", "Nachricht und Optionen"),
|
1 => L("SenderWizard_Step_Receivers", "Empfänger"),
|
||||||
2 => L("SenderEditor_EnvelopeSettings", "Envelope-Einstellungen"),
|
2 => L("SenderEditor_EnvelopeSettings", "Envelope-Einstellungen"),
|
||||||
3 => L("SenderWizard_Step_Receivers", "Empfänger"),
|
3 => L("SenderWizard_Step_Message", "Nachricht und Optionen"),
|
||||||
4 => L("SenderWizard_Step_Document", "Dokument"),
|
4 => L("SenderWizard_Step_Document", "Dokument"),
|
||||||
_ => L("SenderWizard_Step_Basics", "Grunddaten")
|
_ => L("SenderWizard_Step_Basics", "Grunddaten")
|
||||||
};
|
};
|
||||||
|
|
||||||
private string CurrentStepDescription => _currentStep switch
|
private string GetStepDescription(int step) => step switch
|
||||||
{
|
{
|
||||||
0 => L("SenderWizard_Step_Basics_Desc", "Starten Sie mit den Pflichtangaben für den Umschlag."),
|
0 => L("SenderWizard_Step_Basics_Desc", "Starten Sie mit den Pflichtangaben für den Umschlag."),
|
||||||
1 => L("SenderWizard_Step_Message_Desc", "Fügen Sie optionale Nachricht und Einstellungen hinzu."),
|
1 => L("SenderWizard_Step_Receivers_Desc", "Definieren Sie die Empfänger für den Signaturprozess."),
|
||||||
2 => L("SenderWizard_Step_Settings_Desc", "Konfigurieren Sie Sicherheits-, Erinnerungs- und Ablaufoptionen."),
|
2 => L("SenderWizard_Step_Settings_Desc", "Konfigurieren Sie Sicherheits-, Erinnerungs- und Ablaufoptionen."),
|
||||||
3 => L("SenderWizard_Step_Receivers_Desc", "Definieren Sie die Empfänger für den Signaturprozess."),
|
3 => L("SenderWizard_Step_Message_Desc", "Fügen Sie optionale Nachricht und Einstellungen hinzu."),
|
||||||
4 => L("SenderWizard_Step_Document_Desc", "Laden Sie das Dokument hoch und platzieren Sie Signaturfelder."),
|
4 => L("SenderWizard_Step_Document_Desc", "Laden Sie das Dokument hoch und platzieren Sie Signaturfelder."),
|
||||||
_ => L("SenderWizard_Step_Basics_Desc", "Starten Sie mit den Pflichtangaben für den Umschlag.")
|
_ => L("SenderWizard_Step_Basics_Desc", "Starten Sie mit den Pflichtangaben für den Umschlag.")
|
||||||
};
|
};
|
||||||
@@ -704,7 +716,7 @@
|
|||||||
{
|
{
|
||||||
_currentStep++;
|
_currentStep++;
|
||||||
|
|
||||||
if (_currentStep == 3)
|
if (_currentStep == 1)
|
||||||
await EnsureRecentSuggestionsLoadedAsync();
|
await EnsureRecentSuggestionsLoadedAsync();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -798,10 +810,10 @@
|
|||||||
if (!string.Equals(args.Key, "Enter", StringComparison.OrdinalIgnoreCase))
|
if (!string.Equals(args.Key, "Enter", StringComparison.OrdinalIgnoreCase))
|
||||||
return;
|
return;
|
||||||
|
|
||||||
if (_currentStep == 1)
|
if (_currentStep == 3)
|
||||||
return;
|
return;
|
||||||
|
|
||||||
if (_currentStep == 3 && (!string.IsNullOrWhiteSpace(_receiverName) || !string.IsNullOrWhiteSpace(_receiverEmail)))
|
if (_currentStep == 1 && (!string.IsNullOrWhiteSpace(_receiverName) || !string.IsNullOrWhiteSpace(_receiverEmail)))
|
||||||
{
|
{
|
||||||
await AddReceiver();
|
await AddReceiver();
|
||||||
return;
|
return;
|
||||||
@@ -895,7 +907,7 @@
|
|||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (_currentStep == 3)
|
if (_currentStep == 1)
|
||||||
{
|
{
|
||||||
if (_receivers.Count == 0)
|
if (_receivers.Count == 0)
|
||||||
{
|
{
|
||||||
@@ -1116,7 +1128,7 @@
|
|||||||
_language = "de-DE";
|
_language = "de-DE";
|
||||||
_useAccessCode = false;
|
_useAccessCode = false;
|
||||||
_tfaEnabled = false;
|
_tfaEnabled = false;
|
||||||
_certificationType = (int)CertificationType.AdvancedElectronicSignature;
|
_certificationType = CertificationType.AdvancedElectronicSignature;
|
||||||
_finalEmailToCreator = (int)FinalEmailType.No;
|
_finalEmailToCreator = (int)FinalEmailType.No;
|
||||||
_finalEmailToReceivers = (int)FinalEmailType.No;
|
_finalEmailToReceivers = (int)FinalEmailType.No;
|
||||||
_sendReminderEmails = false;
|
_sendReminderEmails = false;
|
||||||
|
|||||||
@@ -1,3 +1,5 @@
|
|||||||
|
using EnvelopeGenerator.Domain.Constants;
|
||||||
|
|
||||||
namespace EnvelopeGenerator.Server.Client.Components.Sender;
|
namespace EnvelopeGenerator.Server.Client.Components.Sender;
|
||||||
|
|
||||||
public sealed record CreateEnvelopeWizardResult(
|
public sealed record CreateEnvelopeWizardResult(
|
||||||
@@ -7,7 +9,7 @@ public sealed record CreateEnvelopeWizardResult(
|
|||||||
string Language,
|
string Language,
|
||||||
bool UseAccessCode,
|
bool UseAccessCode,
|
||||||
bool TFAEnabled,
|
bool TFAEnabled,
|
||||||
int CertificationType,
|
CertificationType CertificationType,
|
||||||
int FinalEmailToCreator,
|
int FinalEmailToCreator,
|
||||||
int FinalEmailToReceivers,
|
int FinalEmailToReceivers,
|
||||||
bool SendReminderEmails,
|
bool SendReminderEmails,
|
||||||
|
|||||||
@@ -12,6 +12,7 @@
|
|||||||
</PropertyGroup>
|
</PropertyGroup>
|
||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
|
<PackageReference Include="MudBlazor" Version="8.0.0" />
|
||||||
<PackageReference Include="DevExpress.Blazor.PdfViewer" Version="25.2.3" />
|
<PackageReference Include="DevExpress.Blazor.PdfViewer" Version="25.2.3" />
|
||||||
<PackageReference Include="DevExpress.Blazor.Reporting.JSBasedControls" Version="25.2.3" />
|
<PackageReference Include="DevExpress.Blazor.Reporting.JSBasedControls" Version="25.2.3" />
|
||||||
<PackageReference Include="DevExpress.Blazor.Reporting.Viewer" Version="25.2.3" />
|
<PackageReference Include="DevExpress.Blazor.Reporting.Viewer" Version="25.2.3" />
|
||||||
|
|||||||
@@ -7,11 +7,12 @@
|
|||||||
@using EnvelopeGenerator.Application.Resources
|
@using EnvelopeGenerator.Application.Resources
|
||||||
@using EnvelopeGenerator.Domain.Constants
|
@using EnvelopeGenerator.Domain.Constants
|
||||||
@using EnvelopeGenerator.Domain.Resources
|
@using EnvelopeGenerator.Domain.Resources
|
||||||
|
@using EnvelopeGenerator.Application.Common.Configurations
|
||||||
@using EnvelopeGenerator.Server.Client.Components.PdfEditor
|
@using EnvelopeGenerator.Server.Client.Components.PdfEditor
|
||||||
@using EnvelopeGenerator.Server.Client.Services
|
@using EnvelopeGenerator.Server.Client.Services
|
||||||
@using Microsoft.AspNetCore.WebUtilities
|
|
||||||
@using Microsoft.AspNetCore.Components.Forms
|
@using Microsoft.AspNetCore.Components.Forms
|
||||||
@using Microsoft.Extensions.Localization
|
@using Microsoft.Extensions.Localization
|
||||||
|
@using Microsoft.Extensions.Options
|
||||||
@inject IJSRuntime JSRuntime
|
@inject IJSRuntime JSRuntime
|
||||||
@inject NavigationManager NavigationManager
|
@inject NavigationManager NavigationManager
|
||||||
@inject AppVersionService AppVersion
|
@inject AppVersionService AppVersion
|
||||||
@@ -19,23 +20,19 @@
|
|||||||
@inject DocumentService DocumentService
|
@inject DocumentService DocumentService
|
||||||
@inject PdfRenderService PdfRenderService
|
@inject PdfRenderService PdfRenderService
|
||||||
@inject ReceiverService ReceiverService
|
@inject ReceiverService ReceiverService
|
||||||
|
@inject EnvelopeReceiverService EnvelopeReceiverService
|
||||||
@inject EnvelopeService EnvelopeService
|
@inject EnvelopeService EnvelopeService
|
||||||
@inject EnvelopeTypeService EnvelopeTypeService
|
@inject EnvelopeTypeService EnvelopeTypeService
|
||||||
@inject CreateEnvelopeBootstrapStorageService CreateEnvelopeBootstrapStorageService
|
|
||||||
@inject AuthService AuthService
|
@inject AuthService AuthService
|
||||||
@inject IStringLocalizer<Resource> UiLocalizer
|
@inject IStringLocalizer<Resource> UiLocalizer
|
||||||
@inject IStringLocalizer<Model> ModelLocalizer
|
@inject IStringLocalizer<Model> ModelLocalizer
|
||||||
|
@inject IOptionsMonitor<SignatureFieldParams> SignatureFieldOptions
|
||||||
|
|
||||||
<link href="_content/DevExpress.Blazor.Themes/blazing-berry.bs5.min.css" rel="stylesheet" />
|
<link href="_content/DevExpress.Blazor.Themes/blazing-berry.bs5.min.css" rel="stylesheet" />
|
||||||
<link href="_content/DevExpress.Blazor.Viewer/css/dx-blazor-viewer-components.bs5.css" rel="stylesheet" />
|
<link href="_content/DevExpress.Blazor.Viewer/css/dx-blazor-viewer-components.bs5.css" rel="stylesheet" />
|
||||||
<link href="@AppVersion.GetVersionedUrl("css/envelope-viewer.css")" rel="stylesheet" />
|
<link href="@AppVersion.GetVersionedUrl("css/envelope-viewer.css")" rel="stylesheet" />
|
||||||
<script src="@AppVersion.GetVersionedUrl("js/envelope-editor.js")"></script>
|
<script src="@AppVersion.GetVersionedUrl("js/envelope-editor.js")"></script>
|
||||||
|
|
||||||
<DxLoadingPanel Visible="@IsBootstrapLoading"
|
|
||||||
IsContentBlocked="true"
|
|
||||||
ApplyBackgroundShading="true"
|
|
||||||
IndicatorAreaVisible="true"
|
|
||||||
Text='@L("SenderEditor_BootstrapLoadingHint", "Der Umschlag wird vorbereitet und die Daten werden uebernommen.")'>
|
|
||||||
<div class="envelope-viewer-layout">
|
<div class="envelope-viewer-layout">
|
||||||
|
|
||||||
@* ── Action Bar ── *@
|
@* ── Action Bar ── *@
|
||||||
@@ -126,9 +123,8 @@
|
|||||||
<div class="sender-settings-field">
|
<div class="sender-settings-field">
|
||||||
<label class="sender-settings-label">@L("CertificationType", "Zertifizierung")</label>
|
<label class="sender-settings-label">@L("CertificationType", "Zertifizierung")</label>
|
||||||
<select @bind="_certificationType" class="sender-settings-select">
|
<select @bind="_certificationType" class="sender-settings-select">
|
||||||
<option value="1">@L("AdvancedElectronicSignature", "Advanced Electronic Signature")</option>
|
<option value="@CertificationType.AdvancedElectronicSignature">@L("AdvancedElectronicSignature", "Advanced Electronic Signature")</option>
|
||||||
<option value="2">@L("ElectronicSignature", "Electronic Signature")</option>
|
<option value="@CertificationType.ElectronicSignature">@L("ElectronicSignature", "Electronic Signature")</option>
|
||||||
<option value="3">@L("QualifiedSignature", "Qualified Signature")</option>
|
|
||||||
</select>
|
</select>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -196,20 +192,43 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="sender-receivers-panel">
|
<div class="sender-receivers-panel">
|
||||||
<div class="sender-receivers-panel__header">
|
<div class="sender-receivers-list">
|
||||||
<div class="sender-receivers-panel__title-wrap">
|
<button class="pdf-toolbar__btn pdf-toolbar__btn--signature-change sender-toolbar-action-btn sender-receivers-list__add-btn"
|
||||||
<span class="sender-receivers-panel__title">@L("Receiver", "Empfänger")</span>
|
|
||||||
<span class="sender-receivers-panel__count">@_receivers.Count</span>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<button class="pdf-toolbar__btn pdf-toolbar__btn--signature-change sender-toolbar-action-btn"
|
|
||||||
@onclick="OpenAddReceiverPopup"
|
@onclick="OpenAddReceiverPopup"
|
||||||
title='@L("AddReceiver", "Empfänger hinzufügen")'>
|
title='@L("AddReceiver", "Empfänger hinzufügen")'>
|
||||||
<svg xmlns="http://www.w3.org/2000/svg" width="14" height="14" fill="currentColor" viewBox="0 0 16 16">
|
<svg xmlns="http://www.w3.org/2000/svg" width="14" height="14" fill="currentColor" viewBox="0 0 16 16">
|
||||||
<path d="M8 1a.5.5 0 0 1 .5.5v6h6a.5.5 0 0 1 0 1h-6v6a.5.5 0 0 1-1 0v-6h-6a.5.5 0 0 1 0-1h6v-6A.5.5 0 0 1 8 1z" />
|
<path d="M8 1a.5.5 0 0 1 .5.5v6h6a.5.5 0 0 1 0 1h-6v6a.5.5 0 0 1-1 0v-6h-6a.5.5 0 0 1 0-1h6v-6A.5.5 0 0 1 8 1z" />
|
||||||
</svg>
|
</svg>
|
||||||
<span class="pdf-toolbar__btn-text">@L("AddReceiver", "Empfänger hinzufügen")</span>
|
<span class="sender-receivers-list__add-text">@L("AddReceiver", "Empfänger hinzufügen")</span>
|
||||||
|
<span class="badge rounded-pill text-bg-primary sender-receivers-list__count">@_receivers.Count</span>
|
||||||
</button>
|
</button>
|
||||||
|
|
||||||
|
@foreach (var receiver in _receivers)
|
||||||
|
{
|
||||||
|
<div class="sender-receiver-chip" style="--receiver-color:@receiver.Color;">
|
||||||
|
<div class="sender-receiver-chip__body">
|
||||||
|
<div class="sender-receiver-chip__email" title="@receiver.FullName">@receiver.Email</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<button class="pdf-toolbar__btn pdf-toolbar__btn--reset sender-toolbar-action-btn sender-toolbar-action-btn--compact sender-receiver-chip__action sender-receiver-chip__remove-btn"
|
||||||
|
@onclick="() => RemoveReceiverAsync(receiver)"
|
||||||
|
title='@L("Delete", "Loeschen")'>
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" width="14" height="14" fill="currentColor" viewBox="0 0 16 16">
|
||||||
|
<path d="M4 4L12 12M12 4L4 12" stroke="currentColor" stroke-width="2" stroke-linecap="round" />
|
||||||
|
</svg>
|
||||||
|
</button>
|
||||||
|
|
||||||
|
<button class="pdf-toolbar__btn pdf-toolbar__btn--signature-change sender-toolbar-action-btn sender-toolbar-action-btn--compact sender-receiver-chip__action sender-receiver-chip__place-btn
|
||||||
|
@(_pendingReceiverForPlacement?.Id == receiver.Id ? "sender-receiver-chip__place-btn--active" : "sender-receiver-chip__place-btn--idle")"
|
||||||
|
@onclick="() => ActivatePlacementForReceiver(receiver)"
|
||||||
|
style="--receiver-color:@receiver.Color;"
|
||||||
|
title='@L("SenderEditor_PlaceSignatureField", "Signaturfeld platzieren")'>
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" width="14" height="14" fill="currentColor" viewBox="0 0 16 16">
|
||||||
|
<path d="M12.146.146a.5.5 0 0 1 .708 0l3 3a.5.5 0 0 1 0 .708l-10 10a.5.5 0 0 1-.168.11l-5 2a.5.5 0 0 1-.65-.65l2-5a.5.5 0 0 1 .11-.168l10-10z" />
|
||||||
|
</svg>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@if (_receivers.Count == 0)
|
@if (_receivers.Count == 0)
|
||||||
@@ -218,35 +237,6 @@
|
|||||||
@L("SenderEditor_NoReceiversAdded", "Noch keine Empfänger hinzugefügt.")
|
@L("SenderEditor_NoReceiversAdded", "Noch keine Empfänger hinzugefügt.")
|
||||||
</div>
|
</div>
|
||||||
}
|
}
|
||||||
else
|
|
||||||
{
|
|
||||||
<div class="sender-receivers-list">
|
|
||||||
@foreach (var receiver in _receivers)
|
|
||||||
{
|
|
||||||
<div class="sender-receiver-chip" style="--receiver-color:@receiver.Color;">
|
|
||||||
<div class="sender-receiver-chip__body">
|
|
||||||
<div class="sender-receiver-chip__name">@receiver.FullName</div>
|
|
||||||
<div class="sender-receiver-chip__email">@receiver.Email</div>
|
|
||||||
@if (!string.IsNullOrWhiteSpace(receiver.PhoneNumber))
|
|
||||||
{
|
|
||||||
<div class="sender-receiver-chip__phone">@receiver.PhoneNumber</div>
|
|
||||||
}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<button class="pdf-toolbar__btn pdf-toolbar__btn--signature-change sender-toolbar-action-btn sender-toolbar-action-btn--compact sender-receiver-chip__place-btn
|
|
||||||
@(_pendingReceiverForPlacement?.Id == receiver.Id ? "sender-receiver-chip__place-btn--active" : "sender-receiver-chip__place-btn--idle")"
|
|
||||||
@onclick="() => ActivatePlacementForReceiver(receiver)"
|
|
||||||
style="--receiver-color:@receiver.Color;"
|
|
||||||
title='@L("SenderEditor_PlaceSignatureField", "Signaturfeld platzieren")'>
|
|
||||||
<svg xmlns="http://www.w3.org/2000/svg" width="14" height="14" fill="currentColor" viewBox="0 0 16 16">
|
|
||||||
<path d="M12.146.146a.5.5 0 0 1 .708 0l3 3a.5.5 0 0 1 0 .708l-10 10a.5.5 0 0 1-.168.11l-5 2a.5.5 0 0 1-.65-.65l2-5a.5.5 0 0 1 .11-.168l10-10z" />
|
|
||||||
</svg>
|
|
||||||
<span class="pdf-toolbar__btn-text">@L("SenderEditor_AddSignature", "Signatur hinzufügen")</span>
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
}
|
|
||||||
</div>
|
|
||||||
}
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -431,7 +421,6 @@
|
|||||||
}
|
}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</DxLoadingPanel>
|
|
||||||
|
|
||||||
<DxPopup @bind-Visible="_receiverPopupVisible"
|
<DxPopup @bind-Visible="_receiverPopupVisible"
|
||||||
HeaderText='@L("AddReceiver", "Empfänger hinzufügen")'
|
HeaderText='@L("AddReceiver", "Empfänger hinzufügen")'
|
||||||
@@ -596,13 +585,11 @@
|
|||||||
[SupplyParameterFromQuery(Name = "envelopeId")]
|
[SupplyParameterFromQuery(Name = "envelopeId")]
|
||||||
public int? EnvelopeId { get; set; }
|
public int? EnvelopeId { get; set; }
|
||||||
|
|
||||||
[SupplyParameterFromQuery(Name = "bootstrapToken")]
|
// ── Signature field size (configurable) ──
|
||||||
public string? BootstrapToken { get; set; }
|
double SigWidthInches => SignatureFieldOptions.CurrentValue.Width;
|
||||||
|
double SigHeightInches => SignatureFieldOptions.CurrentValue.Height;
|
||||||
// ── Constants ──
|
double SigWidthPt => SigWidthInches * 72.0;
|
||||||
// Signature field size in PDF points (fixed): 1.77" × 1.96"
|
double SigHeightPt => SigHeightInches * 72.0;
|
||||||
const double SigWidthPt = 1.77 * 72; // 127.44 pt
|
|
||||||
const double SigHeightPt = 1.96 * 72; // 141.12 pt
|
|
||||||
|
|
||||||
// CssClass for JS click coordinate helper
|
// CssClass for JS click coordinate helper
|
||||||
const string ViewerCssClass = "sender-editor-pdf-viewer";
|
const string ViewerCssClass = "sender-editor-pdf-viewer";
|
||||||
@@ -617,10 +604,7 @@
|
|||||||
int _pageCount;
|
int _pageCount;
|
||||||
List<PdfPageImageDto> _pagePreviews = [];
|
List<PdfPageImageDto> _pagePreviews = [];
|
||||||
int? _loadedEnvelopeId;
|
int? _loadedEnvelopeId;
|
||||||
int? _editingEnvelopeId;
|
|
||||||
bool _isLoadingEnvelopeForEdit;
|
bool _isLoadingEnvelopeForEdit;
|
||||||
bool _bootstrapApplied;
|
|
||||||
string? _pendingBootstrapToken;
|
|
||||||
|
|
||||||
List<SignatureFieldDraft> _signatureFields = [];
|
List<SignatureFieldDraft> _signatureFields = [];
|
||||||
ReceiverDraft? _pendingReceiverForPlacement; // Set when user clicks "Signatur hinzufügen"
|
ReceiverDraft? _pendingReceiverForPlacement; // Set when user clicks "Signatur hinzufügen"
|
||||||
@@ -641,7 +625,6 @@
|
|||||||
string _saveSuccessTitle = "Vorgang erfolgreich abgeschlossen.";
|
string _saveSuccessTitle = "Vorgang erfolgreich abgeschlossen.";
|
||||||
string _saveSuccessBody = string.Empty;
|
string _saveSuccessBody = string.Empty;
|
||||||
bool _saveNavigateToDashboard = false;
|
bool _saveNavigateToDashboard = false;
|
||||||
bool _saveDraftAfterBootstrap;
|
|
||||||
|
|
||||||
// ── Envelope metadata ──
|
// ── Envelope metadata ──
|
||||||
string _envelopeTitle = string.Empty;
|
string _envelopeTitle = string.Empty;
|
||||||
@@ -653,7 +636,7 @@
|
|||||||
string _language = "de-DE";
|
string _language = "de-DE";
|
||||||
bool _useAccessCode;
|
bool _useAccessCode;
|
||||||
bool _tfaEnabled;
|
bool _tfaEnabled;
|
||||||
int _certificationType = (int)CertificationType.AdvancedElectronicSignature;
|
CertificationType _certificationType = CertificationType.AdvancedElectronicSignature;
|
||||||
int _finalEmailToCreator = (int)FinalEmailType.No;
|
int _finalEmailToCreator = (int)FinalEmailType.No;
|
||||||
int _finalEmailToReceivers = (int)FinalEmailType.No;
|
int _finalEmailToReceivers = (int)FinalEmailType.No;
|
||||||
bool _sendReminderEmails;
|
bool _sendReminderEmails;
|
||||||
@@ -675,9 +658,6 @@
|
|||||||
|
|
||||||
static readonly System.ComponentModel.DataAnnotations.EmailAddressAttribute ReceiverEmailValidator = new();
|
static readonly System.ComponentModel.DataAnnotations.EmailAddressAttribute ReceiverEmailValidator = new();
|
||||||
|
|
||||||
bool IsEditMode => EnvelopeId.HasValue && EnvelopeId.Value > 0;
|
|
||||||
bool IsBootstrapLoading => !IsEditMode && !_bootstrapApplied;
|
|
||||||
|
|
||||||
// ── Lifecycle ──
|
// ── Lifecycle ──
|
||||||
|
|
||||||
protected override async Task OnInitializedAsync()
|
protected override async Task OnInitializedAsync()
|
||||||
@@ -692,12 +672,6 @@
|
|||||||
try
|
try
|
||||||
{
|
{
|
||||||
_envelopeTypes = (await EnvelopeTypeService.GetAsync()).ToList();
|
_envelopeTypes = (await EnvelopeTypeService.GetAsync()).ToList();
|
||||||
|
|
||||||
if (!IsEditMode && _selectedEnvelopeTypeId is null && _envelopeTypes.Count > 0)
|
|
||||||
{
|
|
||||||
_selectedEnvelopeTypeId = _envelopeTypes[0].Id;
|
|
||||||
ApplyEnvelopeTypeDefaults(_envelopeTypes[0]);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
catch (Exception ex)
|
catch (Exception ex)
|
||||||
{
|
{
|
||||||
@@ -708,95 +682,14 @@
|
|||||||
|
|
||||||
protected override async Task OnParametersSetAsync()
|
protected override async Task OnParametersSetAsync()
|
||||||
{
|
{
|
||||||
if (EnvelopeId.HasValue && EnvelopeId.Value > 0)
|
if (!EnvelopeId.HasValue || EnvelopeId.Value <= 0)
|
||||||
_editingEnvelopeId = EnvelopeId.Value;
|
|
||||||
|
|
||||||
if (IsEditMode && _loadedEnvelopeId != EnvelopeId)
|
|
||||||
{
|
|
||||||
await LoadEnvelopeForEditAsync(EnvelopeId!.Value);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (IsEditMode || _bootstrapApplied)
|
|
||||||
return;
|
|
||||||
|
|
||||||
if (string.IsNullOrWhiteSpace(BootstrapToken))
|
|
||||||
{
|
{
|
||||||
NavigationManager.NavigateTo("/sender", replace: true);
|
NavigationManager.NavigateTo("/sender", replace: true);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
_pendingBootstrapToken = BootstrapToken;
|
if (_loadedEnvelopeId != EnvelopeId && !_isLoadingEnvelopeForEdit)
|
||||||
}
|
await LoadEnvelopeForEditAsync(EnvelopeId.Value);
|
||||||
|
|
||||||
async Task TryApplyBootstrapAsync(string token)
|
|
||||||
{
|
|
||||||
var payload = await CreateEnvelopeBootstrapStorageService.ConsumeAsync(token);
|
|
||||||
_bootstrapApplied = true;
|
|
||||||
|
|
||||||
if (payload is null)
|
|
||||||
{
|
|
||||||
NavigationManager.NavigateTo("/sender", replace: true);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
_envelopeTitle = payload.Title ?? string.Empty;
|
|
||||||
_envelopeMessage = payload.Message ?? string.Empty;
|
|
||||||
_selectedEnvelopeTypeId = payload.EnvelopeTypeId;
|
|
||||||
_language = string.IsNullOrWhiteSpace(payload.Language) ? "de-DE" : payload.Language;
|
|
||||||
_useAccessCode = payload.UseAccessCode;
|
|
||||||
_tfaEnabled = payload.TFAEnabled;
|
|
||||||
_certificationType = payload.CertificationType;
|
|
||||||
_finalEmailToCreator = payload.FinalEmailToCreator;
|
|
||||||
_finalEmailToReceivers = payload.FinalEmailToReceivers;
|
|
||||||
_sendReminderEmails = payload.SendReminderEmails;
|
|
||||||
_firstReminderDays = payload.FirstReminderDays;
|
|
||||||
_reminderIntervalDays = payload.ReminderIntervalDays;
|
|
||||||
_expiresWhenDays = payload.ExpiresWhenDays;
|
|
||||||
_expiresWarningWhenDays = payload.ExpiresWarningWhenDays;
|
|
||||||
NormalizeSettings();
|
|
||||||
|
|
||||||
_receivers = payload.Receivers
|
|
||||||
.Select((receiver, index) => new ReceiverDraft(
|
|
||||||
Guid.NewGuid(),
|
|
||||||
receiver.FullName,
|
|
||||||
receiver.EmailAddress,
|
|
||||||
string.Empty,
|
|
||||||
ReceiverPalette[index % ReceiverPalette.Length]))
|
|
||||||
.ToList();
|
|
||||||
|
|
||||||
_originalPdfBytes = payload.DocumentBytes;
|
|
||||||
_fileName = string.IsNullOrWhiteSpace(payload.DocumentFileName) ? "document.pdf" : payload.DocumentFileName;
|
|
||||||
_pdfLoaded = _originalPdfBytes is { Length: > 0 };
|
|
||||||
|
|
||||||
_signatureFields.Clear();
|
|
||||||
_pendingReceiverForPlacement = null;
|
|
||||||
|
|
||||||
_saveDraftAfterBootstrap = true;
|
|
||||||
|
|
||||||
if (_pdfLoaded && _originalPdfBytes is not null)
|
|
||||||
{
|
|
||||||
try
|
|
||||||
{
|
|
||||||
_pagePreviews = (await PdfRenderService.RenderPagesAsync(_originalPdfBytes))
|
|
||||||
.OrderBy(p => p.PageNumber)
|
|
||||||
.ToList();
|
|
||||||
}
|
|
||||||
catch (Exception ex)
|
|
||||||
{
|
|
||||||
Logger.LogWarning(ex, "Failed to render bootstrap PDF previews.");
|
|
||||||
_pagePreviews = [];
|
|
||||||
}
|
|
||||||
|
|
||||||
_pageCount = _pagePreviews.Count > 0
|
|
||||||
? _pagePreviews.Count
|
|
||||||
: EstimatePdfPageCount(_originalPdfBytes) ?? 1;
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
_pagePreviews = [];
|
|
||||||
_pageCount = 0;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
protected override async Task OnAfterRenderAsync(bool firstRender)
|
protected override async Task OnAfterRenderAsync(bool firstRender)
|
||||||
@@ -806,26 +699,6 @@
|
|||||||
_isPageReady = true;
|
_isPageReady = true;
|
||||||
await InvokeAsync(StateHasChanged);
|
await InvokeAsync(StateHasChanged);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!OperatingSystem.IsBrowser())
|
|
||||||
return;
|
|
||||||
|
|
||||||
if (!IsEditMode && !_bootstrapApplied && !string.IsNullOrWhiteSpace(_pendingBootstrapToken))
|
|
||||||
{
|
|
||||||
var token = _pendingBootstrapToken;
|
|
||||||
_pendingBootstrapToken = null;
|
|
||||||
await TryApplyBootstrapAsync(token);
|
|
||||||
await InvokeAsync(StateHasChanged);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (_saveDraftAfterBootstrap && !_isSaving)
|
|
||||||
{
|
|
||||||
_saveDraftAfterBootstrap = false;
|
|
||||||
await SaveDraftAndStayOnEditorAsync();
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async Task LoadEnvelopeForEditAsync(int envelopeId)
|
async Task LoadEnvelopeForEditAsync(int envelopeId)
|
||||||
@@ -853,7 +726,7 @@
|
|||||||
if (_tfaEnabled)
|
if (_tfaEnabled)
|
||||||
_useAccessCode = true;
|
_useAccessCode = true;
|
||||||
|
|
||||||
_certificationType = envelope.CertificationType ?? (int)CertificationType.AdvancedElectronicSignature;
|
_certificationType = envelope.CertificationType ?? CertificationType.AdvancedElectronicSignature;
|
||||||
_finalEmailToCreator = envelope.FinalEmailToCreator ?? (int)FinalEmailType.No;
|
_finalEmailToCreator = envelope.FinalEmailToCreator ?? (int)FinalEmailType.No;
|
||||||
_finalEmailToReceivers = envelope.FinalEmailToReceivers ?? (int)FinalEmailType.No;
|
_finalEmailToReceivers = envelope.FinalEmailToReceivers ?? (int)FinalEmailType.No;
|
||||||
|
|
||||||
@@ -869,12 +742,17 @@
|
|||||||
_receivers = envelopeReceivers
|
_receivers = envelopeReceivers
|
||||||
.Select((er, index) => new ReceiverDraft(
|
.Select((er, index) => new ReceiverDraft(
|
||||||
Guid.NewGuid(),
|
Guid.NewGuid(),
|
||||||
|
er.ReceiverId,
|
||||||
er.Name ?? er.Receiver?.EmailAddress ?? $"Receiver {er.ReceiverId}",
|
er.Name ?? er.Receiver?.EmailAddress ?? $"Receiver {er.ReceiverId}",
|
||||||
er.Receiver?.EmailAddress ?? string.Empty,
|
er.Receiver?.EmailAddress ?? string.Empty,
|
||||||
string.Empty,
|
string.Empty,
|
||||||
ReceiverPalette[index % ReceiverPalette.Length]))
|
ReceiverPalette[index % ReceiverPalette.Length]))
|
||||||
.ToList();
|
.ToList();
|
||||||
|
|
||||||
|
var receiverDraftByReceiverId = _receivers
|
||||||
|
.Where(r => r.ReceiverId.HasValue)
|
||||||
|
.ToDictionary(r => r.ReceiverId!.Value, r => r);
|
||||||
|
|
||||||
var receiverById = envelopeReceivers.ToDictionary(er => er.ReceiverId, er => er);
|
var receiverById = envelopeReceivers.ToDictionary(er => er.ReceiverId, er => er);
|
||||||
var colorByEmail = _receivers
|
var colorByEmail = _receivers
|
||||||
.Where(r => !string.IsNullOrWhiteSpace(r.Email))
|
.Where(r => !string.IsNullOrWhiteSpace(r.Email))
|
||||||
@@ -896,10 +774,17 @@
|
|||||||
? receiverColor
|
? receiverColor
|
||||||
: ReceiverPalette[Math.Abs(element.ReceiverId) % ReceiverPalette.Length];
|
: ReceiverPalette[Math.Abs(element.ReceiverId) % ReceiverPalette.Length];
|
||||||
|
|
||||||
|
var receiverDraft = receiverDraftByReceiverId.TryGetValue(element.ReceiverId, out var draft)
|
||||||
|
? draft
|
||||||
|
: _receivers.FirstOrDefault(r =>
|
||||||
|
(!string.IsNullOrWhiteSpace(receiverEmail) && string.Equals(r.Email, receiverEmail, StringComparison.OrdinalIgnoreCase))
|
||||||
|
|| string.Equals(r.FullName, receiverName, StringComparison.OrdinalIgnoreCase));
|
||||||
|
|
||||||
return new SignatureFieldDraft(
|
return new SignatureFieldDraft(
|
||||||
XPt: element.X * 72.0,
|
XPt: element.X * 72.0,
|
||||||
YPt: element.Y * 72.0,
|
YPt: element.Y * 72.0,
|
||||||
Page: element.Page,
|
Page: element.Page,
|
||||||
|
ReceiverDraftId: receiverDraft?.Id ?? Guid.Empty,
|
||||||
ReceiverName: receiverName,
|
ReceiverName: receiverName,
|
||||||
Color: color);
|
Color: color);
|
||||||
})
|
})
|
||||||
@@ -935,7 +820,6 @@
|
|||||||
_fileName = $"Envelope-{envelopeId}.pdf";
|
_fileName = $"Envelope-{envelopeId}.pdf";
|
||||||
_pendingReceiverForPlacement = null;
|
_pendingReceiverForPlacement = null;
|
||||||
_loadedEnvelopeId = envelopeId;
|
_loadedEnvelopeId = envelopeId;
|
||||||
_editingEnvelopeId = envelopeId;
|
|
||||||
}
|
}
|
||||||
catch (Exception ex)
|
catch (Exception ex)
|
||||||
{
|
{
|
||||||
@@ -1135,6 +1019,7 @@
|
|||||||
XPt: xPt,
|
XPt: xPt,
|
||||||
YPt: yPt,
|
YPt: yPt,
|
||||||
Page: page1Based,
|
Page: page1Based,
|
||||||
|
ReceiverDraftId: _pendingReceiverForPlacement.Id,
|
||||||
ReceiverName: _pendingReceiverForPlacement.FullName,
|
ReceiverName: _pendingReceiverForPlacement.FullName,
|
||||||
Color: _pendingReceiverForPlacement.Color);
|
Color: _pendingReceiverForPlacement.Color);
|
||||||
|
|
||||||
@@ -1237,9 +1122,8 @@
|
|||||||
|
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
var previousEnvelopeId = EnvelopeId;
|
|
||||||
var command = BuildCreateEnvelopeCommand(send: false);
|
var command = BuildCreateEnvelopeCommand(send: false);
|
||||||
Logger.LogInformation("[SenderEditor] Saving draft. EnvelopeId={EnvelopeId} IsEditMode={IsEditMode}", command.EnvelopeId, IsEditMode);
|
Logger.LogInformation("[SenderEditor] Saving draft. EnvelopeId={EnvelopeId}", command.EnvelopeId);
|
||||||
var result = await EnvelopeService.UpsertAsync(command);
|
var result = await EnvelopeService.UpsertAsync(command);
|
||||||
|
|
||||||
if (result is null)
|
if (result is null)
|
||||||
@@ -1247,16 +1131,9 @@
|
|||||||
|
|
||||||
EnvelopeId = result.Id;
|
EnvelopeId = result.Id;
|
||||||
_loadedEnvelopeId = result.Id;
|
_loadedEnvelopeId = result.Id;
|
||||||
_editingEnvelopeId = result.Id;
|
|
||||||
|
|
||||||
if (navigateToDashboard)
|
if (navigateToDashboard)
|
||||||
{
|
|
||||||
NavigationManager.NavigateTo("/sender", forceLoad: false);
|
NavigationManager.NavigateTo("/sender", forceLoad: false);
|
||||||
}
|
|
||||||
else if (previousEnvelopeId != result.Id)
|
|
||||||
{
|
|
||||||
NavigationManager.NavigateTo($"/sender/editor?envelopeId={result.Id}", forceLoad: false);
|
|
||||||
}
|
|
||||||
|
|
||||||
return result.Id;
|
return result.Id;
|
||||||
}
|
}
|
||||||
@@ -1274,12 +1151,6 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async Task SaveDraftAndStayOnEditorAsync()
|
|
||||||
{
|
|
||||||
Logger.LogInformation("[SenderEditor] Trigger bootstrap draft save.");
|
|
||||||
await SaveDraftCoreAsync(navigateToDashboard: false);
|
|
||||||
}
|
|
||||||
|
|
||||||
// ── Send envelope ──
|
// ── Send envelope ──
|
||||||
async Task SendAsync()
|
async Task SendAsync()
|
||||||
{
|
{
|
||||||
@@ -1319,7 +1190,7 @@
|
|||||||
|
|
||||||
// Warn if any receiver has no signature field, but don't block
|
// Warn if any receiver has no signature field, but don't block
|
||||||
var receiversWithoutField = _receivers
|
var receiversWithoutField = _receivers
|
||||||
.Where(r => !_signatureFields.Any(f => f.ReceiverName == r.FullName))
|
.Where(r => !_signatureFields.Any(f => f.ReceiverDraftId == r.Id))
|
||||||
.ToList();
|
.ToList();
|
||||||
if (receiversWithoutField.Count > 0)
|
if (receiversWithoutField.Count > 0)
|
||||||
{
|
{
|
||||||
@@ -1334,21 +1205,10 @@
|
|||||||
|
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
var envelopeId = _editingEnvelopeId
|
var envelopeId = EnvelopeId ?? _loadedEnvelopeId;
|
||||||
?? EnvelopeId
|
|
||||||
?? _loadedEnvelopeId
|
|
||||||
?? GetEnvelopeIdFromCurrentUri();
|
|
||||||
|
|
||||||
if (!envelopeId.HasValue || envelopeId.Value <= 0)
|
if (!envelopeId.HasValue || envelopeId.Value <= 0)
|
||||||
{
|
throw new InvalidOperationException("Envelope id is missing for send.");
|
||||||
var saveResult = await EnvelopeService.UpsertAsync(BuildCreateEnvelopeCommand(send: false))
|
|
||||||
?? throw new InvalidOperationException("Draft save did not return a result.");
|
|
||||||
|
|
||||||
envelopeId = saveResult.Id;
|
|
||||||
EnvelopeId = saveResult.Id;
|
|
||||||
_loadedEnvelopeId = saveResult.Id;
|
|
||||||
_editingEnvelopeId = saveResult.Id;
|
|
||||||
}
|
|
||||||
|
|
||||||
var sendResult = await EnvelopeService.UpsertAsync(BuildCreateEnvelopeCommand(send: true) with { EnvelopeId = envelopeId })
|
var sendResult = await EnvelopeService.UpsertAsync(BuildCreateEnvelopeCommand(send: true) with { EnvelopeId = envelopeId })
|
||||||
?? throw new InvalidOperationException("Send did not return a result.");
|
?? throw new InvalidOperationException("Send did not return a result.");
|
||||||
@@ -1379,13 +1239,10 @@
|
|||||||
{
|
{
|
||||||
NormalizeSettings();
|
NormalizeSettings();
|
||||||
|
|
||||||
var envelopeId = _editingEnvelopeId
|
var envelopeId = EnvelopeId ?? _loadedEnvelopeId;
|
||||||
?? EnvelopeId
|
|
||||||
?? _loadedEnvelopeId
|
|
||||||
?? GetEnvelopeIdFromCurrentUri();
|
|
||||||
|
|
||||||
if (IsEditMode && envelopeId is null)
|
if (envelopeId is null)
|
||||||
throw new InvalidOperationException("Edit mode envelope id is missing.");
|
throw new InvalidOperationException("Envelope id is missing.");
|
||||||
|
|
||||||
var docBase64 = _originalPdfBytes is { Length: > 0 }
|
var docBase64 = _originalPdfBytes is { Length: > 0 }
|
||||||
? Convert.ToBase64String(_originalPdfBytes)
|
? Convert.ToBase64String(_originalPdfBytes)
|
||||||
@@ -1394,7 +1251,7 @@
|
|||||||
var receiverCommands = _receivers.Select(receiver =>
|
var receiverCommands = _receivers.Select(receiver =>
|
||||||
{
|
{
|
||||||
var fields = _signatureFields
|
var fields = _signatureFields
|
||||||
.Where(f => f.ReceiverName == receiver.FullName)
|
.Where(f => f.ReceiverDraftId == receiver.Id)
|
||||||
.Select(f => new CreateEnvelopeFieldDto(
|
.Select(f => new CreateEnvelopeFieldDto(
|
||||||
X: f.XPt / 72.0,
|
X: f.XPt / 72.0,
|
||||||
Y: f.YPt / 72.0,
|
Y: f.YPt / 72.0,
|
||||||
@@ -1473,28 +1330,6 @@
|
|||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
int? GetEnvelopeIdFromCurrentUri()
|
|
||||||
{
|
|
||||||
try
|
|
||||||
{
|
|
||||||
var uri = NavigationManager.ToAbsoluteUri(NavigationManager.Uri);
|
|
||||||
if (string.IsNullOrWhiteSpace(uri.Query))
|
|
||||||
return null;
|
|
||||||
|
|
||||||
var query = QueryHelpers.ParseQuery(uri.Query);
|
|
||||||
if (!query.TryGetValue("envelopeId", out var raw))
|
|
||||||
return null;
|
|
||||||
|
|
||||||
return int.TryParse(raw.ToString(), out var id) && id > 0
|
|
||||||
? id
|
|
||||||
: null;
|
|
||||||
}
|
|
||||||
catch
|
|
||||||
{
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// ── Receiver popup ──
|
// ── Receiver popup ──
|
||||||
void OpenAddReceiverPopup()
|
void OpenAddReceiverPopup()
|
||||||
{
|
{
|
||||||
@@ -1656,18 +1491,49 @@
|
|||||||
return Task.CompletedTask;
|
return Task.CompletedTask;
|
||||||
}
|
}
|
||||||
|
|
||||||
_receivers.Add(new ReceiverDraft(Guid.NewGuid(), fullName, email, phoneNumber,
|
_receivers.Add(new ReceiverDraft(Guid.NewGuid(), null, fullName, email, phoneNumber,
|
||||||
ReceiverPalette[_receivers.Count % ReceiverPalette.Length]));
|
ReceiverPalette[_receivers.Count % ReceiverPalette.Length]));
|
||||||
CloseAddReceiverPopup();
|
CloseAddReceiverPopup();
|
||||||
return Task.CompletedTask;
|
return Task.CompletedTask;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async Task RemoveReceiverAsync(ReceiverDraft receiver)
|
||||||
|
{
|
||||||
|
if (_isSaving)
|
||||||
|
return;
|
||||||
|
|
||||||
|
var envelopeId = EnvelopeId ?? _loadedEnvelopeId;
|
||||||
|
|
||||||
|
if (envelopeId is int persistedEnvelopeId && persistedEnvelopeId > 0 && receiver.ReceiverId is int persistedReceiverId && persistedReceiverId > 0)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
await EnvelopeReceiverService.DeleteAsync(persistedEnvelopeId, persistedReceiverId);
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
Logger.LogError(ex, "[SenderEditor] Failed to remove receiver {ReceiverId} from envelope {EnvelopeId}", persistedReceiverId, persistedEnvelopeId);
|
||||||
|
_saveErrorMessage = string.Format(
|
||||||
|
L("SenderEditor_RemoveReceiverError", "Empfaenger konnte nicht entfernt werden: {0}"),
|
||||||
|
ex.Message);
|
||||||
|
_savePopupVisible = true;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
_receivers.RemoveAll(r => r.Id == receiver.Id);
|
||||||
|
_signatureFields.RemoveAll(f => f.ReceiverDraftId == receiver.Id);
|
||||||
|
|
||||||
|
if (_pendingReceiverForPlacement?.Id == receiver.Id)
|
||||||
|
_pendingReceiverForPlacement = null;
|
||||||
|
}
|
||||||
|
|
||||||
// ── Models ──
|
// ── Models ──
|
||||||
record SignatureFieldDraft(double XPt, double YPt, int Page, string ReceiverName, string Color);
|
record SignatureFieldDraft(double XPt, double YPt, int Page, Guid ReceiverDraftId, string ReceiverName, string Color);
|
||||||
|
|
||||||
record NormalisedCoords(double NormX, double NormY, int PageIndex);
|
record NormalisedCoords(double NormX, double NormY, int PageIndex);
|
||||||
|
|
||||||
record ReceiverDraft(Guid Id, string FullName, string Email, string PhoneNumber, string Color);
|
record ReceiverDraft(Guid Id, int? ReceiverId, string FullName, string Email, string PhoneNumber, string Color);
|
||||||
|
|
||||||
// ── Receiver colour palette (cycles when > 8 receivers) ──
|
// ── Receiver colour palette (cycles when > 8 receivers) ──
|
||||||
static readonly string[] ReceiverPalette =
|
static readonly string[] ReceiverPalette =
|
||||||
|
|||||||
@@ -7,6 +7,7 @@
|
|||||||
@using EnvelopeGenerator.Server.Client.Services
|
@using EnvelopeGenerator.Server.Client.Services
|
||||||
@using EnvelopeGenerator.Application.Common.Dto
|
@using EnvelopeGenerator.Application.Common.Dto
|
||||||
@using EnvelopeGenerator.Application.Common.Dto.History
|
@using EnvelopeGenerator.Application.Common.Dto.History
|
||||||
|
@using EnvelopeGenerator.Application.Envelopes.Commands
|
||||||
@using EnvelopeGenerator.Application.Resources
|
@using EnvelopeGenerator.Application.Resources
|
||||||
@using EnvelopeGenerator.Domain.Resources
|
@using EnvelopeGenerator.Domain.Resources
|
||||||
@using EnvelopeGenerator.Server.Client.Components.Sender
|
@using EnvelopeGenerator.Server.Client.Components.Sender
|
||||||
@@ -20,7 +21,7 @@
|
|||||||
@inject EnvelopeGenerator.Server.Client.Services.UserPreferencesService UserPreferencesService
|
@inject EnvelopeGenerator.Server.Client.Services.UserPreferencesService UserPreferencesService
|
||||||
@inject EnvelopeGenerator.Server.Client.Services.CustomImagesService CustomImagesService
|
@inject EnvelopeGenerator.Server.Client.Services.CustomImagesService CustomImagesService
|
||||||
@inject EnvelopeGenerator.Server.Client.Services.PdfSignatureService PdfSignatureService
|
@inject EnvelopeGenerator.Server.Client.Services.PdfSignatureService PdfSignatureService
|
||||||
@inject EnvelopeGenerator.Server.Client.Services.CreateEnvelopeBootstrapStorageService CreateEnvelopeBootstrapStorageService
|
@inject EnvelopeGenerator.Server.Client.Services.EnvelopeReceiverService EnvelopeReceiverService
|
||||||
@inject ILogger<SenderPage> Logger
|
@inject ILogger<SenderPage> Logger
|
||||||
@inject NavigationManager Navigation
|
@inject NavigationManager Navigation
|
||||||
@inject IJSRuntime JSRuntime
|
@inject IJSRuntime JSRuntime
|
||||||
@@ -315,12 +316,13 @@
|
|||||||
@foreach (var receiver in envelope.EnvelopeReceivers)
|
@foreach (var receiver in envelope.EnvelopeReceivers)
|
||||||
{
|
{
|
||||||
<div style="display: flex; align-items: center; gap: 1rem; padding: 0.5rem; background: white; border-radius: 6px; border: 1px solid #e5e7eb;">
|
<div style="display: flex; align-items: center; gap: 1rem; padding: 0.5rem; background: white; border-radius: 6px; border: 1px solid #e5e7eb;">
|
||||||
@{
|
@{
|
||||||
var isRejected = IsReceiverRejected(envelope, receiver);
|
var isRejected = IsReceiverRejected(envelope, receiver);
|
||||||
var isSigned = IsReceiverSigned(envelope, receiver);
|
var isSigned = IsReceiverSigned(envelope, receiver);
|
||||||
var accessCode = receiver.AccessCode;
|
var accessCode = receiver.AccessCode;
|
||||||
var hasAccessCode = !string.IsNullOrWhiteSpace(accessCode);
|
var hasAccessCode = !string.IsNullOrWhiteSpace(accessCode);
|
||||||
}
|
var hasReceiverEnvelopeUrl = CanOpenReceiverEnvelopeUrl(envelope, receiver);
|
||||||
|
}
|
||||||
<span class="receiver-badge receiver-badge--@(isRejected ? "unsigned" : (isSigned ? "signed" : "unsigned"))" style="min-width: 100px;">
|
<span class="receiver-badge receiver-badge--@(isRejected ? "unsigned" : (isSigned ? "signed" : "unsigned"))" style="min-width: 100px;">
|
||||||
@if (isRejected)
|
@if (isRejected)
|
||||||
{
|
{
|
||||||
@@ -349,27 +351,40 @@
|
|||||||
<strong style="color: #1f2937;">@receiver.Name</strong>
|
<strong style="color: #1f2937;">@receiver.Name</strong>
|
||||||
<span style="color: #6b7280; margin-left: 0.5rem;">@receiver.Receiver?.EmailAddress</span>
|
<span style="color: #6b7280; margin-left: 0.5rem;">@receiver.Receiver?.EmailAddress</span>
|
||||||
</div>
|
</div>
|
||||||
<div class="sender-access-code">
|
<div class="sender-access-code">
|
||||||
<span class="sender-access-code__label">@L("AccessCode", "Access code")</span>
|
<span class="sender-access-code__label">@L("AccessCode", "Access code")</span>
|
||||||
<div class="sender-access-code__row">
|
<div class="sender-access-code__row">
|
||||||
<input class="sender-access-code__value"
|
<input class="sender-access-code__value"
|
||||||
value="@(hasAccessCode ? accessCode : "-")"
|
value="@(hasAccessCode ? accessCode : "-")"
|
||||||
readonly
|
readonly
|
||||||
title="@L("AccessCode", "Access code")" />
|
title="@L("AccessCode", "Access code")" />
|
||||||
<button type="button"
|
<button type="button"
|
||||||
class="sender-access-code__copy"
|
class="sender-access-code__copy"
|
||||||
disabled="@(!hasAccessCode)"
|
disabled="@(!hasAccessCode)"
|
||||||
@onclick='() => CopyAccessCodeAsync(accessCode)'
|
@onclick='() => CopyAccessCodeAsync(accessCode)'
|
||||||
title="@L("Copy", "Copy")">
|
title="@L("Copy", "Copy")">
|
||||||
<svg xmlns="http://www.w3.org/2000/svg" width="14" height="14" fill="currentColor" viewBox="0 0 16 16" aria-hidden="true">
|
<svg xmlns="http://www.w3.org/2000/svg" width="14" height="14" fill="currentColor" viewBox="0 0 16 16" aria-hidden="true">
|
||||||
<path d="M10 1.5A1.5 1.5 0 0 1 11.5 3v1H13A1.5 1.5 0 0 1 14.5 5.5v8A1.5 1.5 0 0 1 13 15H5A1.5 1.5 0 0 1 3.5 13.5V12H3A1.5 1.5 0 0 1 1.5 10.5v-8A1.5 1.5 0 0 1 3 1h7zm.5 2A.5.5 0 0 0 10 3H3a.5.5 0 0 0-.5.5v7A.5.5 0 0 0 3 11h.5V5.5A1.5 1.5 0 0 1 5 4h5.5v-.5zM5 5a.5.5 0 0 0-.5.5v8a.5.5 0 0 0 .5.5h8a.5.5 0 0 0 .5-.5v-8A.5.5 0 0 0 13 5H5z"/>
|
<path d="M10 1.5A1.5 1.5 0 0 1 11.5 3v1H13A1.5 1.5 0 0 1 14.5 5.5v8A1.5 1.5 0 0 1 13 15H5A1.5 1.5 0 0 1 3.5 13.5V12H3A1.5 1.5 0 0 1 1.5 10.5v-8A1.5 1.5 0 0 1 3 1h7zm.5 2A.5.5 0 0 0 10 3H3a.5.5 0 0 0-.5.5v7A.5.5 0 0 0 3 11h.5V5.5A1.5 1.5 0 0 1 5 4h5.5v-.5zM5 5a.5.5 0 0 0-.5.5v8a.5.5 0 0 0 .5.5h8a.5.5 0 0 0 .5-.5v-8A.5.5 0 0 0 13 5H5z"/>
|
||||||
</svg>
|
</svg>
|
||||||
</button>
|
</button>
|
||||||
</div>
|
@if (_canUseReceiverEnvelopeUrl)
|
||||||
</div>
|
{
|
||||||
</div>
|
<button type="button"
|
||||||
}
|
class="sender-access-code__copy"
|
||||||
</div>
|
disabled="@(!hasReceiverEnvelopeUrl)"
|
||||||
|
@onclick='() => OpenReceiverEnvelopeUrlAsync(envelope, receiver)'
|
||||||
|
title="@L("Open", "Open")">
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" width="14" height="14" fill="currentColor" viewBox="0 0 16 16" aria-hidden="true">
|
||||||
|
<path fill-rule="evenodd" d="M6.364 13.5a1.5 1.5 0 0 1-1.5-1.5V4a1.5 1.5 0 0 1 1.5-1.5h4a.5.5 0 0 1 0 1h-4a.5.5 0 0 0-.5.5v8a.5.5 0 0 0 .5.5h8a.5.5 0 0 0 .5-.5v-4a.5.5 0 0 1 1 0v4a1.5 1.5 0 0 1-1.5 1.5h-8z"/>
|
||||||
|
<path fill-rule="evenodd" d="M8.5 1a.5.5 0 0 1 .5-.5h5a.5.5 0 0 1 .5.5v5a.5.5 0 0 1-1 0V2.707L8.854 7.354a.5.5 0 1 1-.708-.708L12.793 2H9a.5.5 0 0 1-.5-.5z"/>
|
||||||
|
</svg>
|
||||||
|
</button>
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
</div>
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
@@ -544,6 +559,7 @@
|
|||||||
private string _activeTab = "active";
|
private string _activeTab = "active";
|
||||||
private bool _isLoading = true;
|
private bool _isLoading = true;
|
||||||
private bool _isLoggingOut = false;
|
private bool _isLoggingOut = false;
|
||||||
|
private bool _canUseReceiverEnvelopeUrl = false;
|
||||||
private string? _errorMessage;
|
private string? _errorMessage;
|
||||||
private DxGrid? _grid;
|
private DxGrid? _grid;
|
||||||
|
|
||||||
@@ -603,6 +619,8 @@
|
|||||||
try { await ThemeService.LoadFromServerAsync(); }
|
try { await ThemeService.LoadFromServerAsync(); }
|
||||||
catch { /* swallow — default theme remains active */ }
|
catch { /* swallow — default theme remains active */ }
|
||||||
|
|
||||||
|
_canUseReceiverEnvelopeUrl = await EnvelopeReceiverService.GetReceiverEnvelopeUrlFeatureEnabledAsync();
|
||||||
|
|
||||||
// Load logo config — authenticated, so this should succeed
|
// Load logo config — authenticated, so this should succeed
|
||||||
var logos = await CustomImagesService.GetAsync();
|
var logos = await CustomImagesService.GetAsync();
|
||||||
_appLogo = logos.App;
|
_appLogo = logos.App;
|
||||||
@@ -629,7 +647,7 @@
|
|||||||
|
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
_allEnvelopes = await EnvelopeService.GetAsync() ?? [];
|
_allEnvelopes = await EnvelopeService.GetAsync(includeDocAndDocRes: false) ?? [];
|
||||||
|
|
||||||
// Split into active and completed based on effective status
|
// Split into active and completed based on effective status
|
||||||
// (history-aware: rejected envelopes should not stay in active tab).
|
// (history-aware: rejected envelopes should not stay in active tab).
|
||||||
@@ -726,29 +744,44 @@
|
|||||||
|
|
||||||
async Task StartEditorFromWizardAsync(CreateEnvelopeWizardResult result)
|
async Task StartEditorFromWizardAsync(CreateEnvelopeWizardResult result)
|
||||||
{
|
{
|
||||||
var payload = new CreateEnvelopeBootstrapPayload(
|
var docBase64 = result.DocumentBytes is { Length: > 0 }
|
||||||
Title: result.Title,
|
? Convert.ToBase64String(result.DocumentBytes)
|
||||||
Message: result.Message,
|
: null;
|
||||||
EnvelopeTypeId: result.EnvelopeTypeId,
|
|
||||||
Language: result.Language,
|
|
||||||
UseAccessCode: result.UseAccessCode,
|
|
||||||
TFAEnabled: result.TFAEnabled,
|
|
||||||
CertificationType: result.CertificationType,
|
|
||||||
FinalEmailToCreator: result.FinalEmailToCreator,
|
|
||||||
FinalEmailToReceivers: result.FinalEmailToReceivers,
|
|
||||||
SendReminderEmails: result.SendReminderEmails,
|
|
||||||
FirstReminderDays: result.FirstReminderDays,
|
|
||||||
ReminderIntervalDays: result.ReminderIntervalDays,
|
|
||||||
ExpiresWhenDays: result.ExpiresWhenDays,
|
|
||||||
ExpiresWarningWhenDays: result.ExpiresWarningWhenDays,
|
|
||||||
Receivers: result.Receivers
|
|
||||||
.Select(x => new CreateEnvelopeBootstrapReceiver(x.FullName, x.EmailAddress))
|
|
||||||
.ToList(),
|
|
||||||
DocumentBytes: result.DocumentBytes,
|
|
||||||
DocumentFileName: result.DocumentFileName);
|
|
||||||
|
|
||||||
var token = await CreateEnvelopeBootstrapStorageService.StoreAsync(payload);
|
var command = new CreateEnvelopeCommand
|
||||||
Navigation.NavigateTo($"/sender/editor?bootstrapToken={token}");
|
{
|
||||||
|
EnvelopeTypeId = result.EnvelopeTypeId,
|
||||||
|
Title = result.Title,
|
||||||
|
Message = result.Message,
|
||||||
|
Language = string.IsNullOrWhiteSpace(result.Language) ? "de-DE" : result.Language,
|
||||||
|
UseAccessCode = result.UseAccessCode,
|
||||||
|
TFAEnabled = result.TFAEnabled,
|
||||||
|
CertificationType = result.CertificationType,
|
||||||
|
FinalEmailToCreator = result.FinalEmailToCreator,
|
||||||
|
FinalEmailToReceivers = result.FinalEmailToReceivers,
|
||||||
|
SendReminderEmails = result.SendReminderEmails,
|
||||||
|
FirstReminderDays = result.FirstReminderDays,
|
||||||
|
ReminderIntervalDays = result.ReminderIntervalDays,
|
||||||
|
ExpiresWhenDays = result.ExpiresWhenDays,
|
||||||
|
ExpiresWarningWhenDays = result.ExpiresWarningWhenDays,
|
||||||
|
Send = false,
|
||||||
|
Document = string.IsNullOrWhiteSpace(docBase64) ? null : new CreateEnvelopeDocumentDto(docBase64),
|
||||||
|
Receivers = result.Receivers
|
||||||
|
.Select(x => new CreateEnvelopeReceiverDto(x.EmailAddress, x.FullName, null, []))
|
||||||
|
.ToList()
|
||||||
|
};
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var envelope = await EnvelopeService.UpsertAsync(command)
|
||||||
|
?? throw new InvalidOperationException("Envelope creation returned no result.");
|
||||||
|
Navigation.NavigateTo($"/sender/editor?envelopeId={envelope.Id}");
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
Logger.LogError(ex, "Failed to create envelope from wizard.");
|
||||||
|
_errorMessage = ex.Message;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
void EditEnvelope()
|
void EditEnvelope()
|
||||||
@@ -987,6 +1020,35 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async Task OpenReceiverEnvelopeUrlAsync(EnvelopeDto envelope, EnvelopeGenerator.Application.Common.Dto.EnvelopeReceiver.EnvelopeReceiverDto receiver)
|
||||||
|
{
|
||||||
|
if (!_canUseReceiverEnvelopeUrl)
|
||||||
|
return;
|
||||||
|
|
||||||
|
if (!CanOpenReceiverEnvelopeUrl(envelope, receiver))
|
||||||
|
return;
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var receiverEnvelopeUrl = await EnvelopeReceiverService.GetReceiverEnvelopeUrlAsync(
|
||||||
|
envelope.Uuid,
|
||||||
|
receiver.Receiver!.Signature!);
|
||||||
|
|
||||||
|
if (string.IsNullOrWhiteSpace(receiverEnvelopeUrl))
|
||||||
|
return;
|
||||||
|
|
||||||
|
await JSRuntime.InvokeVoidAsync("open", receiverEnvelopeUrl, "_blank", "noopener,noreferrer");
|
||||||
|
}
|
||||||
|
catch
|
||||||
|
{
|
||||||
|
await JSRuntime.InvokeVoidAsync("console.warn", "[Sender] Failed to open receiver envelope URL.");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
static bool CanOpenReceiverEnvelopeUrl(EnvelopeDto envelope, EnvelopeGenerator.Application.Common.Dto.EnvelopeReceiver.EnvelopeReceiverDto receiver)
|
||||||
|
=> !string.IsNullOrWhiteSpace(envelope.Uuid)
|
||||||
|
&& !string.IsNullOrWhiteSpace(receiver.Receiver?.Signature);
|
||||||
|
|
||||||
void OnCustomizeElement(GridCustomizeElementEventArgs e)
|
void OnCustomizeElement(GridCustomizeElementEventArgs e)
|
||||||
{
|
{
|
||||||
// Future: Add custom row coloring based on status if needed
|
// Future: Add custom row coloring based on status if needed
|
||||||
|
|||||||
@@ -1,11 +1,13 @@
|
|||||||
using Microsoft.AspNetCore.Components.WebAssembly.Hosting;
|
using Microsoft.AspNetCore.Components.WebAssembly.Hosting;
|
||||||
using EnvelopeGenerator.Server.Client.Services;
|
using EnvelopeGenerator.Server.Client.Services;
|
||||||
using EnvelopeGenerator.Server.Client.Options;
|
using EnvelopeGenerator.Server.Client.Options;
|
||||||
|
using MudBlazor.Services;
|
||||||
using DevExpress.Blazor.Reporting;
|
using DevExpress.Blazor.Reporting;
|
||||||
using DevExpress.XtraReports.Web.Extensions;
|
using DevExpress.XtraReports.Web.Extensions;
|
||||||
using DevExpress.DataAccess.Web;
|
using DevExpress.DataAccess.Web;
|
||||||
using DevExpress.XtraReports.Services;
|
using DevExpress.XtraReports.Services;
|
||||||
using System.Globalization;
|
using System.Globalization;
|
||||||
|
using EnvelopeGenerator.Application.Common.Configurations;
|
||||||
|
|
||||||
var builder = WebAssemblyHostBuilder.CreateDefault(args);
|
var builder = WebAssemblyHostBuilder.CreateDefault(args);
|
||||||
|
|
||||||
@@ -15,6 +17,7 @@ CultureInfo.DefaultThreadCurrentCulture = cultureInfo;
|
|||||||
CultureInfo.DefaultThreadCurrentUICulture = cultureInfo;
|
CultureInfo.DefaultThreadCurrentUICulture = cultureInfo;
|
||||||
|
|
||||||
builder.Services.AddLocalization();
|
builder.Services.AddLocalization();
|
||||||
|
builder.Services.AddMudServices();
|
||||||
|
|
||||||
// Named HttpClient for API calls (both for services and DevExpress components)
|
// Named HttpClient for API calls (both for services and DevExpress components)
|
||||||
builder.Services.AddHttpClient("EnvelopeGenerator.Server", client =>
|
builder.Services.AddHttpClient("EnvelopeGenerator.Server", client =>
|
||||||
@@ -31,6 +34,8 @@ builder.Services.AddScoped(sp => new HttpClient
|
|||||||
// Configuration Options
|
// Configuration Options
|
||||||
builder.Services.Configure<PdfViewerOptions>(opts =>
|
builder.Services.Configure<PdfViewerOptions>(opts =>
|
||||||
builder.Configuration.GetSection(PdfViewerOptions.SectionName).Bind(opts));
|
builder.Configuration.GetSection(PdfViewerOptions.SectionName).Bind(opts));
|
||||||
|
builder.Services.Configure<SignatureFieldParams>(opts =>
|
||||||
|
builder.Configuration.GetSection(nameof(SignatureFieldParams)).Bind(opts));
|
||||||
|
|
||||||
// Business Services
|
// Business Services
|
||||||
builder.Services.AddScoped<DocumentService>();
|
builder.Services.AddScoped<DocumentService>();
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
using System.Text.Json;
|
using System.Text.Json;
|
||||||
|
using EnvelopeGenerator.Domain.Constants;
|
||||||
using Microsoft.JSInterop;
|
using Microsoft.JSInterop;
|
||||||
|
|
||||||
namespace EnvelopeGenerator.Server.Client.Services;
|
namespace EnvelopeGenerator.Server.Client.Services;
|
||||||
@@ -49,7 +50,7 @@ public sealed record CreateEnvelopeBootstrapPayload(
|
|||||||
string Language,
|
string Language,
|
||||||
bool UseAccessCode,
|
bool UseAccessCode,
|
||||||
bool TFAEnabled,
|
bool TFAEnabled,
|
||||||
int CertificationType,
|
CertificationType CertificationType,
|
||||||
int FinalEmailToCreator,
|
int FinalEmailToCreator,
|
||||||
int FinalEmailToReceivers,
|
int FinalEmailToReceivers,
|
||||||
bool SendReminderEmails,
|
bool SendReminderEmails,
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ using System.Net.Http.Json;
|
|||||||
using System.Text.Json;
|
using System.Text.Json;
|
||||||
using EnvelopeGenerator.Application.EnvelopeReceivers.Commands.CreateEnvelopeReceiver;
|
using EnvelopeGenerator.Application.EnvelopeReceivers.Commands.CreateEnvelopeReceiver;
|
||||||
using EnvelopeGenerator.Server.Client.Models;
|
using EnvelopeGenerator.Server.Client.Models;
|
||||||
|
using Microsoft.AspNetCore.WebUtilities;
|
||||||
|
|
||||||
namespace EnvelopeGenerator.Server.Client.Services;
|
namespace EnvelopeGenerator.Server.Client.Services;
|
||||||
|
|
||||||
@@ -15,6 +16,8 @@ namespace EnvelopeGenerator.Server.Client.Services;
|
|||||||
public class EnvelopeReceiverService(IHttpClientFactory httpClientFactory)
|
public class EnvelopeReceiverService(IHttpClientFactory httpClientFactory)
|
||||||
{
|
{
|
||||||
private static readonly JsonSerializerOptions _jsonOptions = new(JsonSerializerDefaults.Web);
|
private static readonly JsonSerializerOptions _jsonOptions = new(JsonSerializerDefaults.Web);
|
||||||
|
private sealed record ReceiverEnvelopeUrlResponse(string Url);
|
||||||
|
private sealed record ReceiverEnvelopeUrlEnabledResponse(bool Enabled);
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Fetches the envelope receiver data for the given envelope key from the API.
|
/// Fetches the envelope receiver data for the given envelope key from the API.
|
||||||
@@ -63,4 +66,59 @@ public class EnvelopeReceiverService(IHttpClientFactory httpClientFactory)
|
|||||||
|
|
||||||
return await response.Content.ReadFromJsonAsync<CreateEnvelopeReceiverResponse>(_jsonOptions, cancel);
|
return await response.Content.ReadFromJsonAsync<CreateEnvelopeReceiverResponse>(_jsonOptions, cancel);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public async Task<string?> GetReceiverEnvelopeUrlAsync(string envelopeUuid, string receiverSignature, CancellationToken cancel = default)
|
||||||
|
{
|
||||||
|
if (string.IsNullOrWhiteSpace(envelopeUuid) || string.IsNullOrWhiteSpace(receiverSignature))
|
||||||
|
return null;
|
||||||
|
|
||||||
|
using var http = httpClientFactory.CreateClient("EnvelopeGenerator.Server");
|
||||||
|
var url = $"/api/EnvelopeReceiver/receiver-envelope-url?envelopeUuid={Uri.EscapeDataString(envelopeUuid)}&receiverSignature={Uri.EscapeDataString(receiverSignature)}";
|
||||||
|
var response = await http.GetAsync(url, cancel);
|
||||||
|
|
||||||
|
if (!response.IsSuccessStatusCode)
|
||||||
|
return null;
|
||||||
|
|
||||||
|
var payload = await response.Content.ReadFromJsonAsync<ReceiverEnvelopeUrlResponse>(_jsonOptions, cancel);
|
||||||
|
return payload?.Url;
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<bool> GetReceiverEnvelopeUrlFeatureEnabledAsync(CancellationToken cancel = default)
|
||||||
|
{
|
||||||
|
using var http = httpClientFactory.CreateClient("EnvelopeGenerator.Server");
|
||||||
|
var response = await http.GetAsync("/api/EnvelopeReceiver/receiver-envelope-url/enabled", cancel);
|
||||||
|
|
||||||
|
if (!response.IsSuccessStatusCode)
|
||||||
|
return false;
|
||||||
|
|
||||||
|
var payload = await response.Content.ReadFromJsonAsync<ReceiverEnvelopeUrlEnabledResponse>(_jsonOptions, cancel);
|
||||||
|
return payload?.Enabled == true;
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task DeleteAsync(int envelopeId, int receiverId, CancellationToken cancel = default)
|
||||||
|
{
|
||||||
|
if (envelopeId <= 0)
|
||||||
|
throw new ArgumentOutOfRangeException(nameof(envelopeId));
|
||||||
|
if (receiverId <= 0)
|
||||||
|
throw new ArgumentOutOfRangeException(nameof(receiverId));
|
||||||
|
|
||||||
|
using var http = httpClientFactory.CreateClient("EnvelopeGenerator.Server");
|
||||||
|
var url = QueryHelpers.AddQueryString("/api/EnvelopeReceiver", new Dictionary<string, string?>
|
||||||
|
{
|
||||||
|
["EnvelopeId"] = envelopeId.ToString(),
|
||||||
|
["ReceiverId"] = receiverId.ToString()
|
||||||
|
});
|
||||||
|
|
||||||
|
var response = await http.DeleteAsync(url, cancel);
|
||||||
|
if (response.IsSuccessStatusCode)
|
||||||
|
return;
|
||||||
|
|
||||||
|
var body = await response.Content.ReadAsStringAsync(cancel);
|
||||||
|
var statusCode = (int)response.StatusCode;
|
||||||
|
var reasonPhrase = response.ReasonPhrase ?? "Unknown error";
|
||||||
|
throw new HttpRequestException(
|
||||||
|
$"Failed to remove receiver. Status: {statusCode} ({reasonPhrase}){(string.IsNullOrWhiteSpace(body) ? string.Empty : $" - {body}")}",
|
||||||
|
null,
|
||||||
|
response.StatusCode);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -25,6 +25,7 @@ public class EnvelopeService(IHttpClientFactory clientFactory)
|
|||||||
string? uuid = null,
|
string? uuid = null,
|
||||||
bool? onlyActive = null,
|
bool? onlyActive = null,
|
||||||
bool? onlyCompleted = null,
|
bool? onlyCompleted = null,
|
||||||
|
bool? includeDocAndDocRes = null,
|
||||||
CancellationToken cancel = default)
|
CancellationToken cancel = default)
|
||||||
{
|
{
|
||||||
var baseUrl = $"/api/Envelope";
|
var baseUrl = $"/api/Envelope";
|
||||||
@@ -46,6 +47,10 @@ public class EnvelopeService(IHttpClientFactory clientFactory)
|
|||||||
{
|
{
|
||||||
queryParams["OnlyCompleted"] = onlyCompleted.Value.ToString();
|
queryParams["OnlyCompleted"] = onlyCompleted.Value.ToString();
|
||||||
}
|
}
|
||||||
|
if (includeDocAndDocRes.HasValue)
|
||||||
|
{
|
||||||
|
queryParams["IncludeDocAndDocRes"] = includeDocAndDocRes.Value.ToString();
|
||||||
|
}
|
||||||
|
|
||||||
var url = QueryHelpers.AddQueryString(baseUrl, queryParams);
|
var url = QueryHelpers.AddQueryString(baseUrl, queryParams);
|
||||||
|
|
||||||
|
|||||||
@@ -9,6 +9,7 @@
|
|||||||
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/flag-icons@7.2.3/css/flag-icons.min.css" />
|
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/flag-icons@7.2.3/css/flag-icons.min.css" />
|
||||||
<link rel="stylesheet" href="css/app.css" />
|
<link rel="stylesheet" href="css/app.css" />
|
||||||
<link rel="stylesheet" href="css/envelope-viewer.css" />
|
<link rel="stylesheet" href="css/envelope-viewer.css" />
|
||||||
|
<link rel="stylesheet" href="_content/MudBlazor/MudBlazor.min.css" />
|
||||||
<link rel="stylesheet" href="EnvelopeGenerator.Server.styles.css" />
|
<link rel="stylesheet" href="EnvelopeGenerator.Server.styles.css" />
|
||||||
<HeadOutlet />
|
<HeadOutlet />
|
||||||
<!-- Apply saved theme immediately on page load — before WASM boots.
|
<!-- Apply saved theme immediately on page load — before WASM boots.
|
||||||
@@ -37,6 +38,7 @@
|
|||||||
<script src="js/receiver-signature.js?v=9"></script>
|
<script src="js/receiver-signature.js?v=9"></script>
|
||||||
<script src="js/signflow-theme.js"></script>
|
<script src="js/signflow-theme.js"></script>
|
||||||
<script src="js/envelope-editor.js"></script>
|
<script src="js/envelope-editor.js"></script>
|
||||||
|
<script src="_content/MudBlazor/MudBlazor.min.js"></script>
|
||||||
<script src="_framework/blazor.web.js"></script>
|
<script src="_framework/blazor.web.js"></script>
|
||||||
</body>
|
</body>
|
||||||
|
|
||||||
|
|||||||
@@ -23,27 +23,16 @@ namespace EnvelopeGenerator.Server.Controllers;
|
|||||||
/// - 404 Not Found: Die angeforderte Ressource wurde nicht gefunden.
|
/// - 404 Not Found: Die angeforderte Ressource wurde nicht gefunden.
|
||||||
/// - 500 Internal Server Error: Ein unerwarteter Fehler ist aufgetreten.
|
/// - 500 Internal Server Error: Ein unerwarteter Fehler ist aufgetreten.
|
||||||
/// </remarks>
|
/// </remarks>
|
||||||
|
/// <remarks>
|
||||||
|
/// Erstellt eine neue Instanz des EnvelopeControllers.
|
||||||
|
/// </remarks>
|
||||||
|
/// <param name="Logger">Der Logger, der für das Protokollieren von Informationen verwendet wird.</param>
|
||||||
|
/// <param name="Mediator"></param>
|
||||||
[Route("api/[controller]")]
|
[Route("api/[controller]")]
|
||||||
[ApiController]
|
[ApiController]
|
||||||
[Authorize]
|
[Authorize]
|
||||||
public class EnvelopeController : ControllerBase
|
public class EnvelopeController(ILogger<EnvelopeController> Logger, IMediator Mediator) : ControllerBase
|
||||||
{
|
{
|
||||||
public record DeleteEnvelopeRequest(string Reason);
|
|
||||||
|
|
||||||
private readonly ILogger<EnvelopeController> _logger;
|
|
||||||
private readonly IMediator _mediator;
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Erstellt eine neue Instanz des EnvelopeControllers.
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="logger">Der Logger, der für das Protokollieren von Informationen verwendet wird.</param>
|
|
||||||
/// <param name="mediator"></param>
|
|
||||||
public EnvelopeController(ILogger<EnvelopeController> logger, IMediator mediator)
|
|
||||||
{
|
|
||||||
_logger = logger;
|
|
||||||
_mediator = mediator;
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Ruft eine Liste von Umschlägen basierend auf dem Benutzer und den angegebenen Statusfiltern ab.
|
/// Ruft eine Liste von Umschlägen basierend auf dem Benutzer und den angegebenen Statusfiltern ab.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
@@ -58,8 +47,8 @@ public class EnvelopeController : ControllerBase
|
|||||||
[HttpGet]
|
[HttpGet]
|
||||||
public async Task<IActionResult> GetAsync([FromQuery] ReadEnvelopeQuery envelope)
|
public async Task<IActionResult> GetAsync([FromQuery] ReadEnvelopeQuery envelope)
|
||||||
{
|
{
|
||||||
var result = await _mediator.Send(envelope.Authorize(User.GetId()));
|
var result = await Mediator.Send(envelope.Authorize(User.GetId()));
|
||||||
return result.Any() ? Ok(result) : NotFound();
|
return Ok(result ?? []);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
@@ -74,7 +63,7 @@ public class EnvelopeController : ControllerBase
|
|||||||
[HttpGet("doc-result")]
|
[HttpGet("doc-result")]
|
||||||
public async Task<IActionResult> GetDocResultAsync([FromQuery] ReadEnvelopeQuery query, [FromQuery] bool view = false)
|
public async Task<IActionResult> GetDocResultAsync([FromQuery] ReadEnvelopeQuery query, [FromQuery] bool view = false)
|
||||||
{
|
{
|
||||||
var envelopes = await _mediator.Send(query.Authorize(User.GetId()));
|
var envelopes = await Mediator.Send(query.Authorize(User.GetId()));
|
||||||
var envelope = envelopes.FirstOrDefault();
|
var envelope = envelopes.FirstOrDefault();
|
||||||
|
|
||||||
if (envelope is null)
|
if (envelope is null)
|
||||||
@@ -100,22 +89,34 @@ public class EnvelopeController : ControllerBase
|
|||||||
[HttpPost]
|
[HttpPost]
|
||||||
public async Task<IActionResult> CreateAsync([FromBody] CreateEnvelopeCommand command)
|
public async Task<IActionResult> CreateAsync([FromBody] CreateEnvelopeCommand command)
|
||||||
{
|
{
|
||||||
var res = await _mediator.Send(command.WithAuth(User.GetId()));
|
var res = await Mediator.Send(command.WithAuth(User.GetId()));
|
||||||
|
|
||||||
if (res is null)
|
if (res is null)
|
||||||
{
|
{
|
||||||
_logger.LogError("Failed to create envelope. Envelope details: {EnvelopeDetails}", JsonConvert.SerializeObject(command));
|
Logger.LogError("Failed to create envelope. Envelope details: {EnvelopeDetails}", JsonConvert.SerializeObject(command));
|
||||||
return StatusCode(StatusCodes.Status500InternalServerError);
|
return StatusCode(StatusCodes.Status500InternalServerError);
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
return Ok(res);
|
return Ok(res);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
///
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="Reason"></param>
|
||||||
|
public record DeleteEnvelopeRequest(string Reason);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
///
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="id"></param>
|
||||||
|
/// <param name="request"></param>
|
||||||
|
/// <returns></returns>
|
||||||
[Authorize(AuthenticationSchemes = AuthScheme.Sender)]
|
[Authorize(AuthenticationSchemes = AuthScheme.Sender)]
|
||||||
[HttpDelete("{id:int}")]
|
[HttpDelete("{id:int}")]
|
||||||
public async Task<IActionResult> DeleteAsync([FromRoute] int id, [FromBody] DeleteEnvelopeRequest request)
|
public async Task<IActionResult> DeleteAsync([FromRoute] int id, [FromBody] DeleteEnvelopeRequest request)
|
||||||
{
|
{
|
||||||
var result = await _mediator.Send(new DeleteEnvelopeCommand
|
var result = await Mediator.Send(new DeleteEnvelopeCommand
|
||||||
{
|
{
|
||||||
EnvelopeId = id,
|
EnvelopeId = id,
|
||||||
Reason = request?.Reason ?? string.Empty
|
Reason = request?.Reason ?? string.Empty
|
||||||
@@ -133,7 +134,7 @@ public class EnvelopeController : ControllerBase
|
|||||||
[HttpPost("finalize")]
|
[HttpPost("finalize")]
|
||||||
public async Task<IActionResult> FinalizeEnvelopeAsync([FromQuery] FinalizeEnvelopeCommand command)
|
public async Task<IActionResult> FinalizeEnvelopeAsync([FromQuery] FinalizeEnvelopeCommand command)
|
||||||
{
|
{
|
||||||
await _mediator.Send(command.Authorized(User.GetId()));
|
await Mediator.Send(command.Authorized(User.GetId()));
|
||||||
return Ok();
|
return Ok();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -15,6 +15,9 @@ using EnvelopeGenerator.Application.Common.Interfaces.SQLExecutor;
|
|||||||
using EnvelopeGenerator.Server.Extensions;
|
using EnvelopeGenerator.Server.Extensions;
|
||||||
using EnvelopeGenerator.Domain.Constants;
|
using EnvelopeGenerator.Domain.Constants;
|
||||||
using EnvelopeGenerator.Application.EnvelopeReceivers.Commands.CreateEnvelopeReceiver;
|
using EnvelopeGenerator.Application.EnvelopeReceivers.Commands.CreateEnvelopeReceiver;
|
||||||
|
using EnvelopeGenerator.Application.Common.Extensions;
|
||||||
|
using EnvelopeGenerator.Server.Options;
|
||||||
|
using EnvelopeGenerator.Application.EnvelopeReceivers.Commands;
|
||||||
|
|
||||||
namespace EnvelopeGenerator.Server.Controllers;
|
namespace EnvelopeGenerator.Server.Controllers;
|
||||||
|
|
||||||
@@ -24,32 +27,16 @@ namespace EnvelopeGenerator.Server.Controllers;
|
|||||||
/// <remarks>
|
/// <remarks>
|
||||||
/// Dieser Controller bietet Endpunkte für das Abrufen und Verwalten von Umschlagempfängerdaten.
|
/// Dieser Controller bietet Endpunkte für das Abrufen und Verwalten von Umschlagempfängerdaten.
|
||||||
/// </remarks>
|
/// </remarks>
|
||||||
|
/// <remarks>
|
||||||
|
/// Konstruktor für den EnvelopeReceiverController.
|
||||||
|
/// </remarks>
|
||||||
[Route("api/[controller]")]
|
[Route("api/[controller]")]
|
||||||
[Authorize]
|
[Authorize]
|
||||||
[ApiController]
|
[ApiController]
|
||||||
public class EnvelopeReceiverController : ControllerBase
|
public class EnvelopeReceiverController(ILogger<EnvelopeReceiverController> Logger, IMediator Mediator, IMapper Mapper, IEnvelopeExecutor EnvelopeExecutor, IEnvelopeReceiverExecutor ER_Executor, IDocumentExecutor DocExecutor, IOptions<ConnectionString> csOpt, IOptionsMonitor<SenderFeatureAccessOptions> senderFeatAccessOpt) : ControllerBase
|
||||||
{
|
{
|
||||||
private readonly ILogger<EnvelopeReceiverController> _logger;
|
private readonly string _cnnStr = csOpt.Value.Value;
|
||||||
private readonly IMediator _mediator;
|
private readonly IOptionsMonitor<SenderFeatureAccessOptions> SenderFeatAccessOpt = senderFeatAccessOpt;
|
||||||
private readonly IMapper _mapper;
|
|
||||||
private readonly IEnvelopeExecutor _envelopeExecutor;
|
|
||||||
private readonly IEnvelopeReceiverExecutor _erExecutor;
|
|
||||||
private readonly IDocumentExecutor _documentExecutor;
|
|
||||||
private readonly string _cnnStr;
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Konstruktor für den EnvelopeReceiverController.
|
|
||||||
/// </summary>
|
|
||||||
public EnvelopeReceiverController(ILogger<EnvelopeReceiverController> logger, IMediator mediator, IMapper mapper, IEnvelopeExecutor envelopeExecutor, IEnvelopeReceiverExecutor erExecutor, IDocumentExecutor documentExecutor, IOptions<ConnectionString> csOpt)
|
|
||||||
{
|
|
||||||
_logger = logger;
|
|
||||||
_mediator = mediator;
|
|
||||||
_mapper = mapper;
|
|
||||||
_envelopeExecutor = envelopeExecutor;
|
|
||||||
_erExecutor = erExecutor;
|
|
||||||
_documentExecutor = documentExecutor;
|
|
||||||
_cnnStr = csOpt.Value.Value;
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Ruft eine Liste von Umschlagempfängern basierend auf den angegebenen Abfrageparametern ab.
|
/// Ruft eine Liste von Umschlagempfängern basierend auf den angegebenen Abfrageparametern ab.
|
||||||
@@ -69,7 +56,7 @@ public class EnvelopeReceiverController : ControllerBase
|
|||||||
{
|
{
|
||||||
envelopeReceiver = envelopeReceiver with { Username = User.GetUsername() };
|
envelopeReceiver = envelopeReceiver with { Username = User.GetUsername() };
|
||||||
|
|
||||||
var result = await _mediator.Send(envelopeReceiver);
|
var result = await Mediator.Send(envelopeReceiver);
|
||||||
|
|
||||||
return Ok(result);
|
return Ok(result);
|
||||||
}
|
}
|
||||||
@@ -84,7 +71,7 @@ public class EnvelopeReceiverController : ControllerBase
|
|||||||
[HttpGet("{envelopeKey}")]
|
[HttpGet("{envelopeKey}")]
|
||||||
public async Task<IActionResult> GetEnvelopeReceiverOfReceiver([FromRoute] string envelopeKey, CancellationToken cancel)
|
public async Task<IActionResult> GetEnvelopeReceiverOfReceiver([FromRoute] string envelopeKey, CancellationToken cancel)
|
||||||
{
|
{
|
||||||
var er = await _mediator.Send(new ReadEnvelopeReceiverQuery()
|
var er = await Mediator.Send(new ReadEnvelopeReceiverQuery()
|
||||||
{
|
{
|
||||||
Key = envelopeKey
|
Key = envelopeKey
|
||||||
}, cancel);
|
}, cancel);
|
||||||
@@ -108,10 +95,53 @@ public class EnvelopeReceiverController : ControllerBase
|
|||||||
[HttpGet("salute")]
|
[HttpGet("salute")]
|
||||||
public async Task<IActionResult> GetReceiverName([FromQuery] ReadReceiverNameQuery receiver)
|
public async Task<IActionResult> GetReceiverName([FromQuery] ReadReceiverNameQuery receiver)
|
||||||
{
|
{
|
||||||
var name = await _mediator.Send(receiver);
|
var name = await Mediator.Send(receiver);
|
||||||
return name is null ? NotFound() : Ok(name);
|
return name is null ? NotFound() : Ok(name);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
///
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="envelopeUuid"></param>
|
||||||
|
/// <param name="receiverSignature"></param>
|
||||||
|
/// <returns></returns>
|
||||||
|
[Authorize(AuthenticationSchemes = AuthScheme.Sender)]
|
||||||
|
[HttpGet("receiver-envelope-url")]
|
||||||
|
public IActionResult GetReceiverEnvelopeUrl([FromQuery] string envelopeUuid, [FromQuery] string receiverSignature)
|
||||||
|
{
|
||||||
|
if (!CanUseReceiverEnvelopeUrlFeature())
|
||||||
|
return Forbid();
|
||||||
|
|
||||||
|
if (string.IsNullOrWhiteSpace(envelopeUuid) || string.IsNullOrWhiteSpace(receiverSignature))
|
||||||
|
return BadRequest("Envelope uuid and receiver signature are required.");
|
||||||
|
|
||||||
|
var envelopeKey = (envelopeUuid, receiverSignature).ToEnvelopeKey();
|
||||||
|
var absoluteUrl = $"{Request.Scheme}://{Request.Host}/envelope/{Uri.EscapeDataString(envelopeKey)}";
|
||||||
|
|
||||||
|
return Ok(new { url = absoluteUrl });
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
///
|
||||||
|
/// </summary>
|
||||||
|
/// <returns></returns>
|
||||||
|
[Authorize(AuthenticationSchemes = AuthScheme.Sender)]
|
||||||
|
[HttpGet("receiver-envelope-url/enabled")]
|
||||||
|
public IActionResult GetReceiverEnvelopeUrlFeatureEnabled()
|
||||||
|
{
|
||||||
|
return Ok(new { enabled = CanUseReceiverEnvelopeUrlFeature() });
|
||||||
|
}
|
||||||
|
|
||||||
|
private bool CanUseReceiverEnvelopeUrlFeature()
|
||||||
|
{
|
||||||
|
var senderEmail = User.GetEmail();
|
||||||
|
if (string.IsNullOrWhiteSpace(senderEmail))
|
||||||
|
return false;
|
||||||
|
|
||||||
|
var allowedEmails = SenderFeatAccessOpt.CurrentValue.AllowedReceiverEnvelopeUrlEmails;
|
||||||
|
return allowedEmails.Any(email => string.Equals(email?.Trim(), senderEmail, StringComparison.OrdinalIgnoreCase));
|
||||||
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Returns recent receiver suggestions for the authenticated sender.
|
/// Returns recent receiver suggestions for the authenticated sender.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
@@ -122,7 +152,7 @@ public class EnvelopeReceiverController : ControllerBase
|
|||||||
[HttpGet("recent-suggestions")]
|
[HttpGet("recent-suggestions")]
|
||||||
public async Task<IActionResult> GetRecentSuggestions([FromQuery] ReadRecentReceiverSuggestionsQuery query, CancellationToken cancel)
|
public async Task<IActionResult> GetRecentSuggestions([FromQuery] ReadRecentReceiverSuggestionsQuery query, CancellationToken cancel)
|
||||||
{
|
{
|
||||||
var result = await _mediator.Send(query.ForSender(User.GetUsername()), cancel);
|
var result = await Mediator.Send(query.ForSender(User.GetUsername()), cancel);
|
||||||
return Ok(result);
|
return Ok(result);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -169,7 +199,7 @@ public class EnvelopeReceiverController : ControllerBase
|
|||||||
public async Task<IActionResult> CreateAsync([FromBody] CreateEnvelopeReceiverCommand request, CancellationToken cancel)
|
public async Task<IActionResult> CreateAsync([FromBody] CreateEnvelopeReceiverCommand request, CancellationToken cancel)
|
||||||
{
|
{
|
||||||
#region Create Envelope
|
#region Create Envelope
|
||||||
var envelope = await _envelopeExecutor.CreateEnvelopeAsync(User.GetId(), request.Title, request.Message, request.TFAEnabled, cancel);
|
var envelope = await EnvelopeExecutor.CreateEnvelopeAsync(User.GetId(), request.Title, request.Message, request.TFAEnabled, cancel);
|
||||||
#endregion
|
#endregion
|
||||||
|
|
||||||
#region Add receivers
|
#region Add receivers
|
||||||
@@ -178,7 +208,7 @@ public class EnvelopeReceiverController : ControllerBase
|
|||||||
|
|
||||||
foreach (var receiver in request.Receivers)
|
foreach (var receiver in request.Receivers)
|
||||||
{
|
{
|
||||||
var envelopeReceiver = await _erExecutor.AddEnvelopeReceiverAsync(envelope.Uuid, receiver.EmailAddress, receiver.Salution, receiver.PhoneNumber, cancel);
|
var envelopeReceiver = await ER_Executor.AddEnvelopeReceiverAsync(envelope.Uuid, receiver.EmailAddress, receiver.Salution, receiver.PhoneNumber, cancel);
|
||||||
|
|
||||||
if (envelopeReceiver is null)
|
if (envelopeReceiver is null)
|
||||||
unsentReceivers.Add(receiver);
|
unsentReceivers.Add(receiver);
|
||||||
@@ -186,13 +216,13 @@ public class EnvelopeReceiverController : ControllerBase
|
|||||||
sentReceivers.Add(envelopeReceiver);
|
sentReceivers.Add(envelopeReceiver);
|
||||||
}
|
}
|
||||||
|
|
||||||
var res = _mapper.Map<CreateEnvelopeReceiverResponse>(envelope);
|
var res = Mapper.Map<CreateEnvelopeReceiverResponse>(envelope);
|
||||||
res.UnsentReceivers = unsentReceivers;
|
res.UnsentReceivers = unsentReceivers;
|
||||||
res.SentReceiver = _mapper.Map<List<ReceiverDto>>(sentReceivers.Select(er => er.Receiver));
|
res.SentReceiver = Mapper.Map<List<ReceiverDto>>(sentReceivers.Select(er => er.Receiver));
|
||||||
#endregion
|
#endregion
|
||||||
|
|
||||||
#region Add document
|
#region Add document
|
||||||
var document = await _documentExecutor.CreateDocumentAsync(request.Document.DataAsBase64, envelope.Uuid, cancel);
|
var document = await DocExecutor.CreateDocumentAsync(request.Document.DataAsBase64, envelope.Uuid, cancel);
|
||||||
|
|
||||||
if (document is null)
|
if (document is null)
|
||||||
return StatusCode(StatusCodes.Status500InternalServerError, "Document creation is failed.");
|
return StatusCode(StatusCodes.Status500InternalServerError, "Document creation is failed.");
|
||||||
@@ -229,7 +259,7 @@ public class EnvelopeReceiverController : ControllerBase
|
|||||||
{
|
{
|
||||||
bool outSuccess = reader.GetBoolean(0);
|
bool outSuccess = reader.GetBoolean(0);
|
||||||
if (!outSuccess)
|
if (!outSuccess)
|
||||||
_logger.LogWarning(
|
Logger.LogWarning(
|
||||||
"PRSIG_API_ADD_DOC_RECEIVER_ELEM returned OUT_SUCCESS=false. DOC_ID={DocId}, RECEIVER_ID={ReceiverId}, Page={Page}",
|
"PRSIG_API_ADD_DOC_RECEIVER_ELEM returned OUT_SUCCESS=false. DOC_ID={DocId}, RECEIVER_ID={ReceiverId}, Page={Page}",
|
||||||
document.Id, rcv.Id, sign.Page);
|
document.Id, rcv.Id, sign.Page);
|
||||||
}
|
}
|
||||||
@@ -261,7 +291,7 @@ public class EnvelopeReceiverController : ControllerBase
|
|||||||
{
|
{
|
||||||
bool outSuccess = reader.GetBoolean(0);
|
bool outSuccess = reader.GetBoolean(0);
|
||||||
if (!outSuccess)
|
if (!outSuccess)
|
||||||
_logger.LogWarning(
|
Logger.LogWarning(
|
||||||
"PRSIG_API_ADD_HISTORY_STATE returned OUT_SUCCESS=false. EnvelopeUuid={EnvelopeUuid}",
|
"PRSIG_API_ADD_HISTORY_STATE returned OUT_SUCCESS=false. EnvelopeUuid={EnvelopeUuid}",
|
||||||
envelope.Uuid);
|
envelope.Uuid);
|
||||||
}
|
}
|
||||||
@@ -292,4 +322,18 @@ public class EnvelopeReceiverController : ControllerBase
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
///
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="request"></param>
|
||||||
|
/// <param name="cancel"></param>
|
||||||
|
/// <returns></returns>
|
||||||
|
[HttpDelete]
|
||||||
|
[Authorize(AuthenticationSchemes = AuthScheme.Sender)]
|
||||||
|
public async Task<IActionResult> RemoveAsync([FromQuery] RemoveEnvelopeReceiverCommand request, CancellationToken cancel)
|
||||||
|
{
|
||||||
|
request = request.Authenticated(User.GetId());
|
||||||
|
await Mediator.Send(request, cancel);
|
||||||
|
return Ok();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,88 @@
|
|||||||
|
using EnvelopeGenerator.Application.Common.Interfaces.Services.Notification;
|
||||||
|
using EnvelopeGenerator.Domain.Constants;
|
||||||
|
using Microsoft.AspNetCore.Authorization;
|
||||||
|
using Microsoft.AspNetCore.Mvc;
|
||||||
|
|
||||||
|
namespace EnvelopeGenerator.Server.Controllers;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Sends envelope notifications, optionally with randomly generated payloads.
|
||||||
|
/// </summary>
|
||||||
|
/// <remarks>
|
||||||
|
/// Initializes a new instance of the <see cref="NotificationController"/> class.
|
||||||
|
/// </remarks>
|
||||||
|
[ApiController]
|
||||||
|
[Route("api/[controller]")]
|
||||||
|
public class NotificationController(IEnvelopeNotificationService notificationService) : ControllerBase
|
||||||
|
{
|
||||||
|
private const string FallbackEmail = "htek0100@gmail.com";
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Sends the supplied document signed notification.
|
||||||
|
/// </summary>
|
||||||
|
[HttpPost("signed")]
|
||||||
|
public async Task<IActionResult> SendSigned(
|
||||||
|
[FromBody] DocumentSignedNotification notification,
|
||||||
|
[FromQuery] string? email,
|
||||||
|
CancellationToken cancel)
|
||||||
|
{
|
||||||
|
await notificationService.Notify(NormalizeEmail(email), notification, cancel);
|
||||||
|
return Ok();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Sends a randomly generated document signed notification.
|
||||||
|
/// </summary>
|
||||||
|
[HttpPost("signed/random")]
|
||||||
|
public async Task<IActionResult> SendSignedRandom(
|
||||||
|
[FromQuery] string? email,
|
||||||
|
CancellationToken cancel)
|
||||||
|
{
|
||||||
|
await notificationService.Notify(NormalizeEmail(email), new DocumentSignedNotification
|
||||||
|
{
|
||||||
|
DocumentTitle = $"Dokument {Random.Shared.Next(1000, 9999)}",
|
||||||
|
NameReceiver = $"Empfänger {Random.Shared.Next(1, 100)}",
|
||||||
|
IsReadAndConfirm = Random.Shared.Next(2) == 0,
|
||||||
|
}, cancel);
|
||||||
|
return Ok();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Sends the supplied document received notification.
|
||||||
|
/// </summary>
|
||||||
|
[HttpPost("received")]
|
||||||
|
public async Task<IActionResult> SendReceived(
|
||||||
|
[FromBody] DocumentReceivedNotification notification,
|
||||||
|
[FromQuery] string? email,
|
||||||
|
CancellationToken cancel)
|
||||||
|
{
|
||||||
|
await notificationService.Notify(NormalizeEmail(email), notification, cancel);
|
||||||
|
return Ok();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Sends a randomly generated document received notification.
|
||||||
|
/// </summary>
|
||||||
|
[HttpPost("received/random")]
|
||||||
|
public async Task<IActionResult> SendReceivedRandom(
|
||||||
|
[FromQuery] string? email,
|
||||||
|
CancellationToken cancel)
|
||||||
|
{
|
||||||
|
await notificationService.Notify(NormalizeEmail(email), new DocumentReceivedNotification
|
||||||
|
{
|
||||||
|
DocumentTitle = $"Dokument {Random.Shared.Next(1000, 9999)}",
|
||||||
|
NameReceiver = $"Empfänger {Random.Shared.Next(1, 100)}",
|
||||||
|
NameSender = $"Absender {Random.Shared.Next(1, 100)}",
|
||||||
|
IsReadAndConfirm = Random.Shared.Next(2) == 0,
|
||||||
|
AdvancedSignature = Random.Shared.Next(2) == 0,
|
||||||
|
Origin = new Uri($"{Request.Scheme}://{Request.Host}"),
|
||||||
|
EnvelopeUuid = Guid.NewGuid().ToString(),
|
||||||
|
ReceiverSignature = Guid.NewGuid().ToString(),
|
||||||
|
Message = $"Testnachricht {Random.Shared.Next(1000, 9999)}"
|
||||||
|
}, cancel);
|
||||||
|
return Ok();
|
||||||
|
}
|
||||||
|
|
||||||
|
private static string NormalizeEmail(string? email)
|
||||||
|
=> string.IsNullOrWhiteSpace(email) ? FallbackEmail : email;
|
||||||
|
}
|
||||||
@@ -4,13 +4,17 @@ using EnvelopeGenerator.Application.Common.Interfaces.Doc;
|
|||||||
using EnvelopeGenerator.Domain.Constants;
|
using EnvelopeGenerator.Domain.Constants;
|
||||||
using Microsoft.AspNetCore.Authorization;
|
using Microsoft.AspNetCore.Authorization;
|
||||||
using Microsoft.AspNetCore.Mvc;
|
using Microsoft.AspNetCore.Mvc;
|
||||||
|
using Microsoft.Extensions.Caching.Memory;
|
||||||
|
using System.Security.Cryptography;
|
||||||
|
|
||||||
namespace EnvelopeGenerator.Server.Controllers;
|
namespace EnvelopeGenerator.Server.Controllers;
|
||||||
|
|
||||||
[ApiController]
|
[ApiController]
|
||||||
[Route("api/[controller]")]
|
[Route("api/[controller]")]
|
||||||
public class PdfRenderController(IPdfPageImageRenderer renderer) : ControllerBase
|
public class PdfRenderController(IPdfPageImageRenderer renderer, IMemoryCache memoryCache) : ControllerBase
|
||||||
{
|
{
|
||||||
|
private static readonly TimeSpan RenderCacheDuration = TimeSpan.FromMinutes(5);
|
||||||
|
|
||||||
// TODO(perf): Replace JSON/base64 transport for PdfBytes with application/octet-stream (or multipart) and stream-based request parsing to reduce payload size and allocation pressure for large PDFs.
|
// TODO(perf): Replace JSON/base64 transport for PdfBytes with application/octet-stream (or multipart) and stream-based request parsing to reduce payload size and allocation pressure for large PDFs.
|
||||||
[HttpPost("pages")]
|
[HttpPost("pages")]
|
||||||
[Authorize(AuthenticationSchemes = AuthScheme.Sender)]
|
[Authorize(AuthenticationSchemes = AuthScheme.Sender)]
|
||||||
@@ -19,7 +23,19 @@ public class PdfRenderController(IPdfPageImageRenderer renderer) : ControllerBas
|
|||||||
if (request?.PdfBytes is not { Length: > 0 } bytes)
|
if (request?.PdfBytes is not { Length: > 0 } bytes)
|
||||||
return BadRequest("Missing PDF bytes.");
|
return BadRequest("Missing PDF bytes.");
|
||||||
|
|
||||||
|
var cacheKey = BuildCacheKey(bytes);
|
||||||
|
if (memoryCache.TryGetValue(cacheKey, out IReadOnlyList<PdfPageImageDto>? cachedPages) && cachedPages is not null)
|
||||||
|
return Ok(cachedPages);
|
||||||
|
|
||||||
IReadOnlyList<PdfPageImageDto> pages = await renderer.RenderPagesAsImagesAsync(bytes, cancel);
|
IReadOnlyList<PdfPageImageDto> pages = await renderer.RenderPagesAsImagesAsync(bytes, cancel);
|
||||||
|
memoryCache.Set(cacheKey, pages, RenderCacheDuration);
|
||||||
|
|
||||||
return Ok(pages);
|
return Ok(pages);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private static string BuildCacheKey(byte[] pdfBytes)
|
||||||
|
{
|
||||||
|
var hash = Convert.ToHexString(SHA256.HashData(pdfBytes));
|
||||||
|
return $"pdf-render:pages:v1:dpi110:{hash}";
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -10,9 +10,9 @@
|
|||||||
<Authors>Digital Data GmbH</Authors>
|
<Authors>Digital Data GmbH</Authors>
|
||||||
<Company>Digital Data GmbH</Company>
|
<Company>Digital Data GmbH</Company>
|
||||||
<Product>EnvelopeGenerator.Server</Product>
|
<Product>EnvelopeGenerator.Server</Product>
|
||||||
<Version>1.7.0-beta</Version>
|
<Version>1.8.0-beta</Version>
|
||||||
<FileVersion>1.7.0.0</FileVersion>
|
<FileVersion>1.8.0.0</FileVersion>
|
||||||
<AssemblyVersion>1.7.0.0</AssemblyVersion>
|
<AssemblyVersion>1.8.0.0</AssemblyVersion>
|
||||||
<Copyright>Copyright © 2026 Digital Data GmbH. All rights reserved.</Copyright>
|
<Copyright>Copyright © 2026 Digital Data GmbH. All rights reserved.</Copyright>
|
||||||
<DocumentationFile>bin\$(Configuration)\$(TargetFramework)\$(AssemblyName).xml</DocumentationFile>
|
<DocumentationFile>bin\$(Configuration)\$(TargetFramework)\$(AssemblyName).xml</DocumentationFile>
|
||||||
</PropertyGroup>
|
</PropertyGroup>
|
||||||
@@ -30,6 +30,7 @@
|
|||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<PackageReference Include="Microsoft.AspNetCore.Components.WebAssembly.Server" Version="8.0.22" />
|
<PackageReference Include="Microsoft.AspNetCore.Components.WebAssembly.Server" Version="8.0.22" />
|
||||||
|
<PackageReference Include="MudBlazor" Version="8.0.0" />
|
||||||
<PackageReference Include="DevExpress.Blazor" Version="25.2.3" />
|
<PackageReference Include="DevExpress.Blazor" Version="25.2.3" />
|
||||||
<PackageReference Include="DevExpress.Blazor.PdfViewer" Version="25.2.3" />
|
<PackageReference Include="DevExpress.Blazor.PdfViewer" Version="25.2.3" />
|
||||||
<PackageReference Include="DevExpress.Blazor.Reporting.Viewer" Version="25.2.3" />
|
<PackageReference Include="DevExpress.Blazor.Reporting.Viewer" Version="25.2.3" />
|
||||||
|
|||||||
@@ -0,0 +1,8 @@
|
|||||||
|
namespace EnvelopeGenerator.Server.Options;
|
||||||
|
|
||||||
|
public sealed class SenderFeatureAccessOptions
|
||||||
|
{
|
||||||
|
public const string SectionName = "SenderFeatureAccess";
|
||||||
|
|
||||||
|
public List<string> AllowedReceiverEnvelopeUrlEmails { get; set; } = [];
|
||||||
|
}
|
||||||
@@ -8,6 +8,7 @@ using DigitalData.Core.Application;
|
|||||||
using DigitalData.EmailProfilerDispatcher;
|
using DigitalData.EmailProfilerDispatcher;
|
||||||
using DigitalData.UserManager.DependencyInjection;
|
using DigitalData.UserManager.DependencyInjection;
|
||||||
using EnvelopeGenerator.Application;
|
using EnvelopeGenerator.Application;
|
||||||
|
using EnvelopeGenerator.Application.Common.Interfaces;
|
||||||
using EnvelopeGenerator.Application.Common.Interfaces.Doc;
|
using EnvelopeGenerator.Application.Common.Interfaces.Doc;
|
||||||
using EnvelopeGenerator.Domain.Constants;
|
using EnvelopeGenerator.Domain.Constants;
|
||||||
using EnvelopeGenerator.Infrastructure;
|
using EnvelopeGenerator.Infrastructure;
|
||||||
@@ -19,6 +20,7 @@ using EnvelopeGenerator.Server.Client.Services;
|
|||||||
using EnvelopeGenerator.Server.Components;
|
using EnvelopeGenerator.Server.Components;
|
||||||
using EnvelopeGenerator.Server.Models;
|
using EnvelopeGenerator.Server.Models;
|
||||||
using EnvelopeGenerator.Server.Options;
|
using EnvelopeGenerator.Server.Options;
|
||||||
|
using EnvelopeGenerator.Server.Services;
|
||||||
using Microsoft.AspNetCore.Authentication.Cookies;
|
using Microsoft.AspNetCore.Authentication.Cookies;
|
||||||
using Microsoft.AspNetCore.Authentication.JwtBearer;
|
using Microsoft.AspNetCore.Authentication.JwtBearer;
|
||||||
using Microsoft.AspNetCore.Localization;
|
using Microsoft.AspNetCore.Localization;
|
||||||
@@ -30,6 +32,7 @@ using NLog;
|
|||||||
using NLog.Web;
|
using NLog.Web;
|
||||||
using Scalar.AspNetCore;
|
using Scalar.AspNetCore;
|
||||||
using System.Globalization;
|
using System.Globalization;
|
||||||
|
using MudBlazor.Services;
|
||||||
|
|
||||||
var logger = LogManager.Setup().LoadConfigurationFromAppSettings().GetCurrentClassLogger();
|
var logger = LogManager.Setup().LoadConfigurationFromAppSettings().GetCurrentClassLogger();
|
||||||
logger.Info("EnvelopeGenerator.Server logging initialized!");
|
logger.Info("EnvelopeGenerator.Server logging initialized!");
|
||||||
@@ -58,6 +61,7 @@ try
|
|||||||
builder.Services.AddRazorComponents()
|
builder.Services.AddRazorComponents()
|
||||||
.AddInteractiveServerComponents()
|
.AddInteractiveServerComponents()
|
||||||
.AddInteractiveWebAssemblyComponents();
|
.AddInteractiveWebAssemblyComponents();
|
||||||
|
builder.Services.AddMudServices();
|
||||||
|
|
||||||
// Add API Controllers
|
// Add API Controllers
|
||||||
builder.Services.AddControllers()
|
builder.Services.AddControllers()
|
||||||
@@ -298,6 +302,7 @@ try
|
|||||||
|
|
||||||
// Cache options
|
// Cache options
|
||||||
builder.Services.Configure<CacheOptions>(config.GetSection(CacheOptions.SectionName));
|
builder.Services.Configure<CacheOptions>(config.GetSection(CacheOptions.SectionName));
|
||||||
|
builder.Services.Configure<SenderFeatureAccessOptions>(config.GetSection(SenderFeatureAccessOptions.SectionName));
|
||||||
|
|
||||||
// Distributed Cache - SQL Server
|
// Distributed Cache - SQL Server
|
||||||
builder.Services.AddDistributedSqlServerCache(options =>
|
builder.Services.AddDistributedSqlServerCache(options =>
|
||||||
@@ -391,6 +396,8 @@ try
|
|||||||
builder.Services.Configure<EnvelopeGenerator.Server.Client.Options.PdfViewerOptions>(
|
builder.Services.Configure<EnvelopeGenerator.Server.Client.Options.PdfViewerOptions>(
|
||||||
builder.Configuration.GetSection("PdfViewerOptions"));
|
builder.Configuration.GetSection("PdfViewerOptions"));
|
||||||
|
|
||||||
|
builder.Services.AddScoped<IRequestContext, RequestContext>();
|
||||||
|
|
||||||
var app = builder.Build();
|
var app = builder.Build();
|
||||||
|
|
||||||
deferredProvider.Factory = () => app.Services;
|
deferredProvider.Factory = () => app.Services;
|
||||||
|
|||||||
@@ -0,0 +1,18 @@
|
|||||||
|
using EnvelopeGenerator.Application.Common.Interfaces;
|
||||||
|
|
||||||
|
namespace EnvelopeGenerator.Server.Services;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
///
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="HttpContextAccessor"></param>
|
||||||
|
public sealed class RequestContext(
|
||||||
|
IHttpContextAccessor HttpContextAccessor) : IRequestContext
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
///
|
||||||
|
/// </summary>
|
||||||
|
public string? Origin =>
|
||||||
|
HttpContextAccessor.HttpContext?
|
||||||
|
.Request.Headers.Origin.ToString();
|
||||||
|
}
|
||||||
@@ -58,8 +58,12 @@
|
|||||||
"MainCanvasEnableHiDPI": true,
|
"MainCanvasEnableHiDPI": true,
|
||||||
"ZoomStepPercentage": 5
|
"ZoomStepPercentage": 5
|
||||||
},
|
},
|
||||||
|
"SignatureFieldParams": {
|
||||||
|
"Width": 1.77,
|
||||||
|
"Height": 1.96
|
||||||
|
},
|
||||||
"DocumentService": {
|
"DocumentService": {
|
||||||
"BaseUrl": "https://localhost:7186"
|
"BaseUrl": "http://172.24.12.39:9393"
|
||||||
},
|
},
|
||||||
"PSPDFKitLicenseKey": "SXCtGGY9XA-31OGUXQK-r7c6AkdLGPm2ljuyDr1qu0kkhLvydg-Do-fxpNUF4Rq3fS_xAnZRNFRHbXpE6sQ2BMcCSVTcXVJO6tPviexjpiT-HnrDEySlUERJnnvh-tmeOWprxS6BySPnSILkmaVQtUfOIUS-cUbvvEYHTvQBKbSF8di4XHQFyfv49ihr51axm3NVV3AXwh2EiKL5C5XdqBZ4sQ4O7vXBjM2zvxdPxlxdcNYmiU83uAzw7B83O_jubPzya4CdUHh_YH7Nlp2gP56MeG1Sw2JhMtfG3Rj14Sg4ctaeL9p6AEWca5dDjJ2li5tFIV2fQSsw6A_cowLu0gtMm5i8IfJXeIcQbMC2-0wGv1oe9hZYJvFMdzhTM_FiejM0agemxt3lJyzuyP8zbBSOgp7Si6A85krLWPZptyZBTG7pp7IHboUHfPMxCXqi-zMsqewOJtQBE2mjntU-lPryKnssOpMPfswwQX7QSkJYV5EMqNmEhQX6mEkp2wcqFzMC7bJQew1aO4pOpvChUaMvb1vgRek0HxLag0nwQYX2YrYGh7F_xXJs-8HNwJe8H0-eW4x4faayCgM5rB5772CCCsD9ThZcvXFrjNHHLGJ8WuBUFm6LArvSfFQdii_7j-_sqHMpeKZt26NFgivj1A==",
|
"PSPDFKitLicenseKey": "SXCtGGY9XA-31OGUXQK-r7c6AkdLGPm2ljuyDr1qu0kkhLvydg-Do-fxpNUF4Rq3fS_xAnZRNFRHbXpE6sQ2BMcCSVTcXVJO6tPviexjpiT-HnrDEySlUERJnnvh-tmeOWprxS6BySPnSILkmaVQtUfOIUS-cUbvvEYHTvQBKbSF8di4XHQFyfv49ihr51axm3NVV3AXwh2EiKL5C5XdqBZ4sQ4O7vXBjM2zvxdPxlxdcNYmiU83uAzw7B83O_jubPzya4CdUHh_YH7Nlp2gP56MeG1Sw2JhMtfG3Rj14Sg4ctaeL9p6AEWca5dDjJ2li5tFIV2fQSsw6A_cowLu0gtMm5i8IfJXeIcQbMC2-0wGv1oe9hZYJvFMdzhTM_FiejM0agemxt3lJyzuyP8zbBSOgp7Si6A85krLWPZptyZBTG7pp7IHboUHfPMxCXqi-zMsqewOJtQBE2mjntU-lPryKnssOpMPfswwQX7QSkJYV5EMqNmEhQX6mEkp2wcqFzMC7bJQew1aO4pOpvChUaMvb1vgRek0HxLag0nwQYX2YrYGh7F_xXJs-8HNwJe8H0-eW4x4faayCgM5rB5772CCCsD9ThZcvXFrjNHHLGJ8WuBUFm6LArvSfFQdii_7j-_sqHMpeKZt26NFgivj1A==",
|
||||||
"Content-Security-Policy": [ // The first format parameter {0} will be replaced by the nonce value.
|
"Content-Security-Policy": [ // The first format parameter {0} will be replaced by the nonce value.
|
||||||
@@ -208,6 +212,9 @@
|
|||||||
"TableName": "TBDD_CACHE"
|
"TableName": "TBDD_CACHE"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"SenderFeatureAccess": {
|
||||||
|
"AllowedReceiverEnvelopeUrlEmails": ["h.tek@digitaldata.works"]
|
||||||
|
},
|
||||||
"MainPageTitle": null,
|
"MainPageTitle": null,
|
||||||
"AnnotationParams": {
|
"AnnotationParams": {
|
||||||
"Background": {
|
"Background": {
|
||||||
|
|||||||
@@ -14,5 +14,9 @@
|
|||||||
"RenderingOpacity": 0.85,
|
"RenderingOpacity": 0.85,
|
||||||
"ThumbnailRenderDelay": 50,
|
"ThumbnailRenderDelay": 50,
|
||||||
"ZoomStepPercentage": 5
|
"ZoomStepPercentage": 5
|
||||||
|
},
|
||||||
|
"SignatureFieldParams": {
|
||||||
|
"Width": 1.77,
|
||||||
|
"Height": 1.96
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -989,7 +989,7 @@ body.resizing {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.sender-toolbar-action-btn--compact {
|
.sender-toolbar-action-btn--compact {
|
||||||
padding: 0.45rem 0.7rem;
|
padding: 0.28rem 0.42rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
.sender-receivers-panel {
|
.sender-receivers-panel {
|
||||||
@@ -1208,23 +1208,68 @@ body.resizing {
|
|||||||
.sender-receivers-list {
|
.sender-receivers-list {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-wrap: wrap;
|
flex-wrap: wrap;
|
||||||
gap: 0.625rem;
|
gap: 0.4rem;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sender-receivers-list__add-btn {
|
||||||
|
position: relative;
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 0.38rem;
|
||||||
|
min-height: 30px;
|
||||||
|
padding: 0.35rem 0.6rem;
|
||||||
|
margin-right: 0.28rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sender-receivers-list__add-text {
|
||||||
|
font-size: 0.75rem;
|
||||||
|
font-weight: 600;
|
||||||
|
line-height: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sender-receivers-list__count {
|
||||||
|
position: absolute;
|
||||||
|
top: -0.45rem;
|
||||||
|
right: -0.45rem;
|
||||||
|
min-width: 1.15rem;
|
||||||
|
height: 1.15rem;
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
font-size: 0.65rem;
|
||||||
|
font-weight: 700;
|
||||||
|
padding: 0 0.3rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
.sender-receiver-chip {
|
.sender-receiver-chip {
|
||||||
display: inline-flex;
|
display: inline-flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
gap: 0.75rem;
|
gap: 0.35rem;
|
||||||
min-width: 220px;
|
min-width: 190px;
|
||||||
max-width: 100%;
|
max-width: 100%;
|
||||||
padding: 0.625rem 0.75rem;
|
padding: 0.28rem 0.4rem;
|
||||||
border-radius: 12px;
|
border-radius: 9px;
|
||||||
background: rgba(255, 255, 255, 0.88);
|
background: rgba(255, 255, 255, 0.88);
|
||||||
border: 1px solid rgba(126, 34, 206, 0.12);
|
border: 1px solid rgba(126, 34, 206, 0.12);
|
||||||
border-left: 3px solid var(--receiver-color, #7c3aed);
|
border-left: 3px solid var(--receiver-color, #7c3aed);
|
||||||
box-shadow: 0 6px 16px rgba(15, 23, 42, 0.06);
|
box-shadow: 0 6px 16px rgba(15, 23, 42, 0.06);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.sender-receiver-chip__action {
|
||||||
|
min-width: auto;
|
||||||
|
min-height: 26px;
|
||||||
|
width: 26px;
|
||||||
|
height: 26px;
|
||||||
|
padding: 0;
|
||||||
|
border-radius: 6px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sender-receiver-chip__action svg {
|
||||||
|
width: 11px;
|
||||||
|
height: 11px;
|
||||||
|
}
|
||||||
|
|
||||||
.sender-receiver-chip__place-btn--idle {
|
.sender-receiver-chip__place-btn--idle {
|
||||||
color: var(--receiver-color, #7c3aed);
|
color: var(--receiver-color, #7c3aed);
|
||||||
border-color: var(--receiver-color, #7c3aed);
|
border-color: var(--receiver-color, #7c3aed);
|
||||||
@@ -1236,6 +1281,17 @@ body.resizing {
|
|||||||
border-color: var(--receiver-color, #7c3aed);
|
border-color: var(--receiver-color, #7c3aed);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.sender-receiver-chip__remove-btn {
|
||||||
|
color: #b91c1c;
|
||||||
|
border-color: rgba(185, 28, 28, 0.35);
|
||||||
|
}
|
||||||
|
|
||||||
|
.sender-receiver-chip__remove-btn:hover {
|
||||||
|
background: #b91c1c;
|
||||||
|
color: #ffffff;
|
||||||
|
border-color: #b91c1c;
|
||||||
|
}
|
||||||
|
|
||||||
.sender-editor-action-inner {
|
.sender-editor-action-inner {
|
||||||
flex-direction: row;
|
flex-direction: row;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
@@ -1505,8 +1561,9 @@ body.resizing {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.sender-receiver-chip__email {
|
.sender-receiver-chip__email {
|
||||||
margin-top: 0.15rem;
|
margin-top: 0;
|
||||||
font-size: 0.72rem;
|
font-size: 0.7rem;
|
||||||
|
line-height: 1.1;
|
||||||
color: #64748b;
|
color: #64748b;
|
||||||
white-space: nowrap;
|
white-space: nowrap;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
|
|||||||
@@ -1116,57 +1116,71 @@
|
|||||||
z-index: 2;
|
z-index: 2;
|
||||||
}
|
}
|
||||||
|
|
||||||
.sender-create-wizard__progress {
|
.sender-create-wizard__stepper-shell {
|
||||||
position: relative;
|
border: 1px solid rgba(126, 34, 206, 0.12);
|
||||||
display: grid;
|
border-radius: 10px;
|
||||||
grid-template-columns: repeat(4, minmax(0, 1fr));
|
background: rgba(126, 34, 206, 0.04);
|
||||||
align-items: center;
|
padding: 0.75rem;
|
||||||
gap: 0.5rem;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.sender-create-wizard__progress-track {
|
.sender-create-wizard__stepper {
|
||||||
position: absolute;
|
margin: 0;
|
||||||
left: 12px;
|
|
||||||
right: 12px;
|
|
||||||
top: 50%;
|
|
||||||
height: 2px;
|
|
||||||
transform: translateY(-50%);
|
|
||||||
background: rgba(126, 34, 206, 0.18);
|
|
||||||
z-index: 0;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.sender-create-wizard__step {
|
.sender-create-wizard__stepper .mud-step {
|
||||||
position: relative;
|
pointer-events: none;
|
||||||
z-index: 1;
|
|
||||||
display: flex;
|
|
||||||
justify-content: center;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.sender-create-wizard__step-index {
|
.sender-create-wizard__stepper .mud-stepper-actions,
|
||||||
width: 24px;
|
.sender-create-wizard__stepper .mud-stepper-action {
|
||||||
height: 24px;
|
display: none !important;
|
||||||
border-radius: 999px;
|
}
|
||||||
display: inline-flex;
|
|
||||||
align-items: center;
|
.sender-create-wizard__stepper .mud-step-label {
|
||||||
justify-content: center;
|
color: #1f2937;
|
||||||
font-size: 0.78rem;
|
font-size: 0.86rem;
|
||||||
font-weight: 700;
|
font-weight: 700;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sender-create-wizard__stepper .mud-step-content {
|
||||||
|
color: #64748b;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sender-create-wizard__stepper .mud-stepper-vertical .mud-stepper-step {
|
||||||
|
margin-bottom: 0.25rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sender-create-wizard__stepper .mud-step-icon {
|
||||||
color: #64748b;
|
color: #64748b;
|
||||||
background: #f8fafc;
|
background: #f8fafc;
|
||||||
border: 1px solid rgba(126, 34, 206, 0.25);
|
border: 1px solid rgba(126, 34, 206, 0.25);
|
||||||
}
|
}
|
||||||
|
|
||||||
.sender-create-wizard__step--active .sender-create-wizard__step-index {
|
.sender-create-wizard__stepper .mud-step-completed .mud-step-icon,
|
||||||
|
.sender-create-wizard__stepper .mud-step-current .mud-step-icon {
|
||||||
color: #ffffff;
|
color: #ffffff;
|
||||||
background: linear-gradient(135deg, #7e22ce 0%, #2a5298 100%);
|
|
||||||
border-color: transparent;
|
border-color: transparent;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sender-create-wizard__stepper .mud-step-current .mud-step-icon {
|
||||||
|
background: linear-gradient(135deg, #7e22ce 0%, #2a5298 100%);
|
||||||
box-shadow: 0 0 0 3px rgba(126, 34, 206, 0.15);
|
box-shadow: 0 0 0 3px rgba(126, 34, 206, 0.15);
|
||||||
}
|
}
|
||||||
|
|
||||||
.sender-create-wizard__step--done .sender-create-wizard__step-index {
|
.sender-create-wizard__stepper .mud-step-completed .mud-step-icon {
|
||||||
color: #ffffff;
|
|
||||||
background: #2a5298;
|
background: #2a5298;
|
||||||
border-color: transparent;
|
}
|
||||||
|
|
||||||
|
.sender-create-wizard__stepper-description {
|
||||||
|
font-size: 0.8rem;
|
||||||
|
line-height: 1.35;
|
||||||
|
color: #64748b;
|
||||||
|
margin-top: 0.2rem;
|
||||||
|
padding-right: 0.35rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sender-create-wizard__step-content {
|
||||||
|
margin-top: 0.7rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
.sender-create-wizard__meta {
|
.sender-create-wizard__meta {
|
||||||
|
|||||||
@@ -215,7 +215,7 @@ public static class Extensions
|
|||||||
Message = faker.Lorem.Paragraph(faker.Random.Number(2, 5)),
|
Message = faker.Lorem.Paragraph(faker.Random.Number(2, 5)),
|
||||||
TfaEnabled = tfaEnabled,
|
TfaEnabled = tfaEnabled,
|
||||||
AddedWhen = DateTime.Now,
|
AddedWhen = DateTime.Now,
|
||||||
CertificationType = (int)CertificationType.AdvancedElectronicSignature,
|
CertificationType = CertificationType.AdvancedElectronicSignature,
|
||||||
UseAccessCode = false,
|
UseAccessCode = false,
|
||||||
ContractType = (int)ContractType.Contract,
|
ContractType = (int)ContractType.Contract,
|
||||||
Language = "de",
|
Language = "de",
|
||||||
|
|||||||
@@ -71,10 +71,12 @@
|
|||||||
|
|
||||||
<ItemGroup Condition="'$(TargetFramework)' == 'net8.0'">
|
<ItemGroup Condition="'$(TargetFramework)' == 'net8.0'">
|
||||||
<ProjectReference Include="..\EnvelopeGenerator.Infrastructure.Crypt\EnvelopeGenerator.Infrastructure.Crypt.csproj" />
|
<ProjectReference Include="..\EnvelopeGenerator.Infrastructure.Crypt\EnvelopeGenerator.Infrastructure.Crypt.csproj" />
|
||||||
|
<ProjectReference Include="..\EnvelopeGenerator.Infrastructure.Notification\EnvelopeGenerator.Infrastructure.Notification.csproj" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|
||||||
<ItemGroup Condition="'$(TargetFramework)' == 'net7.0'">
|
<ItemGroup Condition="'$(TargetFramework)' == 'net7.0'">
|
||||||
<Compile Remove="Application\Crypt\**\*.cs" />
|
<Compile Remove="Application\Crypt\**\*.cs" />
|
||||||
|
<Compile Remove="Infrastructure\**\*.cs" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|
||||||
<ItemGroup Condition="'$(TargetFramework)' == 'net8.0'">
|
<ItemGroup Condition="'$(TargetFramework)' == 'net8.0'">
|
||||||
|
|||||||
@@ -0,0 +1,145 @@
|
|||||||
|
using System.Text.Json;
|
||||||
|
using System.Text.RegularExpressions;
|
||||||
|
using EnvelopeGenerator.Infrastructure.Notification;
|
||||||
|
using Microsoft.Data.SqlClient;
|
||||||
|
|
||||||
|
namespace EnvelopeGenerator.Tests.Infrastructure;
|
||||||
|
|
||||||
|
[TestFixture]
|
||||||
|
public class MailTemplatePlaceholderDbTests
|
||||||
|
{
|
||||||
|
private static readonly Regex PlaceholderPattern = new(@"\[[A-Z0-9_]+\]", RegexOptions.Compiled);
|
||||||
|
|
||||||
|
private static IEnumerable<TestCaseData> DbTemplateCases()
|
||||||
|
{
|
||||||
|
foreach (var template in ReadTemplates())
|
||||||
|
{
|
||||||
|
yield return new TestCaseData(template)
|
||||||
|
{
|
||||||
|
TestName = $"Template_{template.Name}_{template.LangCode}_{template.Guid}"
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public void Database_contains_templates()
|
||||||
|
{
|
||||||
|
Assert.That(ReadTemplates(), Is.Not.Empty);
|
||||||
|
}
|
||||||
|
|
||||||
|
[TestCaseSource(nameof(DbTemplateCases))]
|
||||||
|
public void Every_placeholder_in_every_db_template_is_replaced(TemplateRow template)
|
||||||
|
{
|
||||||
|
var data = new TemplatePlaceholders();
|
||||||
|
|
||||||
|
var subject = template.Subject.ReplacePlaceholders(data);
|
||||||
|
var body = template.Body.ReplacePlaceholders(data);
|
||||||
|
|
||||||
|
var originalTokens = PlaceholderPattern
|
||||||
|
.Matches(template.Subject + template.Body)
|
||||||
|
.Select(m => m.Value)
|
||||||
|
.Distinct()
|
||||||
|
.ToArray();
|
||||||
|
|
||||||
|
Assert.Multiple(() =>
|
||||||
|
{
|
||||||
|
Assert.That(Unresolved(subject), Is.Empty,
|
||||||
|
$"'{template.Name}' subject has unresolved placeholders.");
|
||||||
|
Assert.That(Unresolved(body), Is.Empty,
|
||||||
|
$"'{template.Name}' body has unresolved placeholders.");
|
||||||
|
|
||||||
|
foreach (var token in originalTokens)
|
||||||
|
{
|
||||||
|
Assert.That(subject + body, Does.Contain(Expected(token)),
|
||||||
|
$"'{template.Name}' placeholder {token} was not replaced with its value.");
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
private static string[] Unresolved(string text)
|
||||||
|
=> PlaceholderPattern.Matches(text)
|
||||||
|
.Select(m => m.Value)
|
||||||
|
.Distinct()
|
||||||
|
.ToArray();
|
||||||
|
|
||||||
|
private static string Expected(string token) => $"__{token.Trim('[', ']')}__";
|
||||||
|
|
||||||
|
private static List<TemplateRow> ReadTemplates()
|
||||||
|
{
|
||||||
|
const string sql = "SELECT GUID, NAME, SUBJECT, BODY, LANG_CODE FROM TBSIG_EMAIL_TEMPLATE";
|
||||||
|
|
||||||
|
using var connection = new SqlConnection(ReadConnectionString());
|
||||||
|
connection.Open();
|
||||||
|
|
||||||
|
using var command = new SqlCommand(sql, connection);
|
||||||
|
using var reader = command.ExecuteReader();
|
||||||
|
|
||||||
|
var templates = new List<TemplateRow>();
|
||||||
|
while (reader.Read())
|
||||||
|
{
|
||||||
|
templates.Add(new TemplateRow(
|
||||||
|
reader.GetInt32(0),
|
||||||
|
reader.GetString(1),
|
||||||
|
reader.IsDBNull(2) ? string.Empty : reader.GetString(2),
|
||||||
|
reader.IsDBNull(3) ? string.Empty : reader.GetString(3),
|
||||||
|
reader.IsDBNull(4) ? string.Empty : reader.GetString(4)));
|
||||||
|
}
|
||||||
|
|
||||||
|
return templates;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static string ReadConnectionString()
|
||||||
|
{
|
||||||
|
var directory = new DirectoryInfo(AppContext.BaseDirectory);
|
||||||
|
while (directory is not null && !File.Exists(Path.Combine(directory.FullName, "EnvelopeGenerator.sln")))
|
||||||
|
directory = directory.Parent;
|
||||||
|
|
||||||
|
if (directory is null)
|
||||||
|
throw new InvalidOperationException("Solution root (EnvelopeGenerator.sln) was not found.");
|
||||||
|
|
||||||
|
var settingsPath = Path.Combine(
|
||||||
|
directory.FullName,
|
||||||
|
"EnvelopeGenerator.Server",
|
||||||
|
"EnvelopeGenerator.Server",
|
||||||
|
"appsettings.json");
|
||||||
|
|
||||||
|
using var stream = File.OpenRead(settingsPath);
|
||||||
|
using var document = JsonDocument.Parse(stream, new JsonDocumentOptions
|
||||||
|
{
|
||||||
|
CommentHandling = JsonCommentHandling.Skip,
|
||||||
|
AllowTrailingCommas = true
|
||||||
|
});
|
||||||
|
|
||||||
|
return document.RootElement
|
||||||
|
.GetProperty("ConnectionStrings")
|
||||||
|
.GetProperty("Default")
|
||||||
|
.GetString()
|
||||||
|
?? throw new InvalidOperationException($"ConnectionStrings:Default is missing in '{settingsPath}'.");
|
||||||
|
}
|
||||||
|
|
||||||
|
public sealed record TemplateRow(int Guid, string Name, string Subject, string Body, string LangCode);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// One property per placeholder used by any row in TBSIG_EMAIL_TEMPLATE.
|
||||||
|
/// The sentinel value equals the placeholder name, so a wrong mapping is detected.
|
||||||
|
/// </summary>
|
||||||
|
private sealed class TemplatePlaceholders
|
||||||
|
{
|
||||||
|
public string DocumentProcess { get; set; } = "__DOCUMENT_PROCESS__";
|
||||||
|
public string DocumentTitle { get; set; } = "__DOCUMENT_TITLE__";
|
||||||
|
public string LinkToDocumentText { get; set; } = "__LINK_TO_DOCUMENT_TEXT__";
|
||||||
|
public string LinkToDocument { get; set; } = "__LINK_TO_DOCUMENT__";
|
||||||
|
public string Message { get; set; } = "__MESSAGE__";
|
||||||
|
public string NamePortal { get; set; } = "__NAME_PORTAL__";
|
||||||
|
public string NameReceiver { get; set; } = "__NAME_RECEIVER__";
|
||||||
|
public string NameSender { get; set; } = "__NAME_SENDER__";
|
||||||
|
public string SignatureType { get; set; } = "__SIGNATURE_TYPE__";
|
||||||
|
public string Reason { get; set; } = "__REASON__";
|
||||||
|
public string ReceiverAction { get; set; } = "__RECEIVER_ACTION__";
|
||||||
|
public string FinalAction { get; set; } = "__FINAL_ACTION__";
|
||||||
|
public string FinalStatus { get; set; } = "__FINAL_STATUS__";
|
||||||
|
public string DocumentAccessCode { get; set; } = "__DOCUMENT_ACCESS_CODE__";
|
||||||
|
public string TfaExpiration { get; set; } = "__TFA_EXPIRATION__";
|
||||||
|
public string TfaQrCode { get; set; } = "__TFA_QR_CODE__";
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,478 @@
|
|||||||
|
using EnvelopeGenerator.Application.Common.Interfaces.Services.Notification;
|
||||||
|
using EnvelopeGenerator.Infrastructure.Notification;
|
||||||
|
|
||||||
|
namespace EnvelopeGenerator.Tests.Infrastructure;
|
||||||
|
|
||||||
|
[TestFixture]
|
||||||
|
public class ReplacePlaceholderExtensionsTests
|
||||||
|
{
|
||||||
|
#region Naming
|
||||||
|
|
||||||
|
private static IEnumerable<TestCaseData> NamingCases()
|
||||||
|
{
|
||||||
|
yield return Naming<PersonModel>("[FIRST_NAME]");
|
||||||
|
yield return Naming<PersonModel>("[LAST_NAME]");
|
||||||
|
yield return Naming<IdModel>("[ID]");
|
||||||
|
yield return Naming<UpperIdModel>("[ID]");
|
||||||
|
yield return Naming<AcronymModel>("[URL_VALUE]");
|
||||||
|
yield return Naming<AcronymModel>("[HTTP_STATUS_CODE]");
|
||||||
|
yield return Naming<AcronymModel>("[XML_HTTP_REQUEST]");
|
||||||
|
yield return Naming<AcronymModel>("[OTP]");
|
||||||
|
yield return Naming<DigitModel>("[FIELD1]");
|
||||||
|
yield return Naming<DigitModel>("[VALUE2_TEXT]");
|
||||||
|
yield return Naming<DigitModel>("[A1_B2]");
|
||||||
|
yield return Naming<DigitModel>("[NAME2]");
|
||||||
|
yield return Naming<MyUrl2ValueModel>("[MY_URL2_VALUE]");
|
||||||
|
yield return Naming<SingleLetterModel>("[X]");
|
||||||
|
yield return Naming<LowerCaseModel>("[FIRST_NAME]");
|
||||||
|
yield return Naming<SnakeCaseModel>("[FIRST_NAME]");
|
||||||
|
yield return Naming<TfaModel>("[TFA_EXPIRATION]");
|
||||||
|
}
|
||||||
|
|
||||||
|
private static TestCaseData Naming<T>(string placeholder) where T : new()
|
||||||
|
=> new(new T(), placeholder)
|
||||||
|
{
|
||||||
|
TestName = $"Placeholder_{typeof(T).Name}_{placeholder.Trim('[', ']')}"
|
||||||
|
};
|
||||||
|
|
||||||
|
[TestCaseSource(nameof(NamingCases))]
|
||||||
|
public void Property_names_are_converted_to_upper_snake_case(object data, string placeholder)
|
||||||
|
{
|
||||||
|
var result = placeholder.ReplacePlaceholders(data);
|
||||||
|
|
||||||
|
Assert.That(result, Is.EqualTo("v"));
|
||||||
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
[SetCulture("tr-TR")]
|
||||||
|
public void Placeholder_names_are_culture_invariant()
|
||||||
|
{
|
||||||
|
var result = "[INFO]".ReplacePlaceholders(new LowerCaseInfoModel());
|
||||||
|
|
||||||
|
Assert.That(result, Is.EqualTo("v"), "Turkish casing would produce [\u0130NFO]");
|
||||||
|
}
|
||||||
|
|
||||||
|
#endregion
|
||||||
|
|
||||||
|
#region Basic replacement
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public void Replaces_single_placeholder()
|
||||||
|
{
|
||||||
|
var result = "[FIRST_NAME]".ReplacePlaceholders(new PersonModel { FirstName = "Ada" });
|
||||||
|
|
||||||
|
Assert.That(result, Is.EqualTo("Ada"));
|
||||||
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public void Replaces_multiple_placeholders()
|
||||||
|
{
|
||||||
|
var data = new PersonModel { FirstName = "Ada", LastName = "Lovelace" };
|
||||||
|
|
||||||
|
var result = "[FIRST_NAME] [LAST_NAME]".ReplacePlaceholders(data);
|
||||||
|
|
||||||
|
Assert.That(result, Is.EqualTo("Ada Lovelace"));
|
||||||
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public void Replaces_all_repeated_occurrences()
|
||||||
|
{
|
||||||
|
var data = new PersonModel { FirstName = "Ada" };
|
||||||
|
|
||||||
|
var result = "[FIRST_NAME]-[FIRST_NAME]-[FIRST_NAME]".ReplacePlaceholders(data);
|
||||||
|
|
||||||
|
Assert.That(result, Is.EqualTo("Ada-Ada-Ada"));
|
||||||
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public void Leaves_unknown_placeholder_untouched()
|
||||||
|
{
|
||||||
|
var result = "[UNKNOWN_TOKEN]".ReplacePlaceholders(new PersonModel());
|
||||||
|
|
||||||
|
Assert.That(result, Is.EqualTo("[UNKNOWN_TOKEN]"));
|
||||||
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public void Returns_template_unchanged_when_it_has_no_placeholders()
|
||||||
|
{
|
||||||
|
var result = "Hello world".ReplacePlaceholders(new PersonModel());
|
||||||
|
|
||||||
|
Assert.That(result, Is.EqualTo("Hello world"));
|
||||||
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public void Returns_empty_string_for_empty_template()
|
||||||
|
{
|
||||||
|
var result = string.Empty.ReplacePlaceholders(new PersonModel());
|
||||||
|
|
||||||
|
Assert.That(result, Is.Empty);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public void Returns_template_unchanged_when_data_has_no_readable_properties()
|
||||||
|
{
|
||||||
|
var result = "[ANYTHING]".ReplacePlaceholders(new object());
|
||||||
|
|
||||||
|
Assert.That(result, Is.EqualTo("[ANYTHING]"));
|
||||||
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public void Matching_is_case_sensitive()
|
||||||
|
{
|
||||||
|
var result = "[name]|[Name]".ReplacePlaceholders(new PersonNameModel { Name = "v" });
|
||||||
|
|
||||||
|
Assert.That(result, Is.EqualTo("[name]|[Name]"));
|
||||||
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public void Placeholder_without_brackets_is_untouched()
|
||||||
|
{
|
||||||
|
var result = "NAME Receiver".ReplacePlaceholders(new PersonNameModel { Name = "v" });
|
||||||
|
|
||||||
|
Assert.That(result, Is.EqualTo("NAME Receiver"));
|
||||||
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public void Double_brackets_leave_the_outer_brackets()
|
||||||
|
{
|
||||||
|
var result = "[[NAME]]".ReplacePlaceholders(new PersonNameModel { Name = "v" });
|
||||||
|
|
||||||
|
Assert.That(result, Is.EqualTo("[v]"));
|
||||||
|
}
|
||||||
|
|
||||||
|
#endregion
|
||||||
|
|
||||||
|
#region Values
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public void Null_value_is_replaced_with_empty_string()
|
||||||
|
{
|
||||||
|
var result = "[NAME]".ReplacePlaceholders(new { Name = (string?)null });
|
||||||
|
|
||||||
|
Assert.That(result, Is.Empty);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public void Custom_ToString_is_used()
|
||||||
|
{
|
||||||
|
var result = "[VALUE]".ReplacePlaceholders(new { Value = new CustomValue() });
|
||||||
|
|
||||||
|
Assert.That(result, Is.EqualTo("custom"));
|
||||||
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public void Primitive_values_use_ToString()
|
||||||
|
{
|
||||||
|
var result = "[COUNT]|[ENABLED]".ReplacePlaceholders(new { Count = 3, Enabled = true });
|
||||||
|
|
||||||
|
Assert.That(result, Is.EqualTo("3|True"));
|
||||||
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public void Regex_like_characters_are_inserted_literally()
|
||||||
|
{
|
||||||
|
var result = "[PATTERN]".ReplacePlaceholders(new { Pattern = @"$1 {0} \ $&" });
|
||||||
|
|
||||||
|
Assert.That(result, Is.EqualTo(@"$1 {0} \ $&"));
|
||||||
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public void Value_containing_its_own_placeholder_is_not_expanded_recursively()
|
||||||
|
{
|
||||||
|
var result = "[NAME]".ReplacePlaceholders(new EchoNameModel());
|
||||||
|
|
||||||
|
Assert.That(result, Is.EqualTo("[NAME]"));
|
||||||
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public void Unicode_value_is_preserved()
|
||||||
|
{
|
||||||
|
var result = "[NAME]".ReplacePlaceholders(new PersonNameModel { Name = "Z\u00fcrich \ud83d\ude00" });
|
||||||
|
|
||||||
|
Assert.That(result, Is.EqualTo("Z\u00fcrich \ud83d\ude00"));
|
||||||
|
}
|
||||||
|
|
||||||
|
#endregion
|
||||||
|
|
||||||
|
#region Property discovery
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public void Public_readable_property_is_replaced()
|
||||||
|
{
|
||||||
|
var result = "[SHOWN]".ReplacePlaceholders(new DiscoveryModel());
|
||||||
|
|
||||||
|
Assert.That(result, Is.EqualTo("shown"));
|
||||||
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public void Get_only_property_is_replaced()
|
||||||
|
{
|
||||||
|
var result = "[READ_ONLY]".ReplacePlaceholders(new DiscoveryModel());
|
||||||
|
|
||||||
|
Assert.That(result, Is.EqualTo("computed"));
|
||||||
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public void Static_private_write_only_and_indexer_properties_are_ignored()
|
||||||
|
{
|
||||||
|
const string template = "[HIDDEN]|[STATIC]|[WRITE_ONLY]|[ITEM]";
|
||||||
|
|
||||||
|
var result = template.ReplacePlaceholders(new DiscoveryModel());
|
||||||
|
|
||||||
|
Assert.That(result, Is.EqualTo(template));
|
||||||
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public void Inherited_public_property_is_replaced()
|
||||||
|
{
|
||||||
|
var result = "[ID]|[NAME]".ReplacePlaceholders(new DerivedModel());
|
||||||
|
|
||||||
|
Assert.That(result, Is.EqualTo("base|derived"));
|
||||||
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public void Record_properties_are_replaced()
|
||||||
|
{
|
||||||
|
var result = "[FIRST_NAME]/[AGE]".ReplacePlaceholders(new PersonRecord("Ada", 36));
|
||||||
|
|
||||||
|
Assert.That(result, Is.EqualTo("Ada/36"));
|
||||||
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public void Struct_data_is_supported()
|
||||||
|
{
|
||||||
|
var result = "[NAME]".ReplacePlaceholders(new StructModel { Name = "struct" });
|
||||||
|
|
||||||
|
Assert.That(result, Is.EqualTo("struct"));
|
||||||
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public void Anonymous_type_is_supported()
|
||||||
|
{
|
||||||
|
var data = new { Name = "anon", Age = 42 };
|
||||||
|
|
||||||
|
var result = "[NAME]|[AGE]".ReplacePlaceholders(data);
|
||||||
|
|
||||||
|
Assert.That(result, Is.EqualTo("anon|42"));
|
||||||
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public void Generic_type_is_supported_for_reference_type()
|
||||||
|
{
|
||||||
|
var result = "[VALUE]".ReplacePlaceholders(new Box<string> { Value = "boxed" });
|
||||||
|
|
||||||
|
Assert.That(result, Is.EqualTo("boxed"));
|
||||||
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public void Generic_type_is_supported_for_value_type()
|
||||||
|
{
|
||||||
|
var result = "[VALUE]".ReplacePlaceholders(new Box<int> { Value = 5 });
|
||||||
|
|
||||||
|
Assert.That(result, Is.EqualTo("5"));
|
||||||
|
}
|
||||||
|
|
||||||
|
#endregion
|
||||||
|
|
||||||
|
#region Cache and thread safety
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public void Cached_metadata_uses_values_of_current_instance()
|
||||||
|
{
|
||||||
|
var first = "[FIRST_NAME]".ReplacePlaceholders(new PersonModel { FirstName = "first" });
|
||||||
|
var second = "[FIRST_NAME]".ReplacePlaceholders(new PersonModel { FirstName = "second" });
|
||||||
|
|
||||||
|
Assert.That(new[] { first, second }, Is.EqualTo(new[] { "first", "second" }));
|
||||||
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public void Two_types_with_same_property_name_are_resolved_independently()
|
||||||
|
{
|
||||||
|
var a = "[NAME]".ReplacePlaceholders(new TypeA());
|
||||||
|
var b = "[NAME]".ReplacePlaceholders(new TypeB());
|
||||||
|
|
||||||
|
Assert.That(new[] { a, b }, Is.EqualTo(new[] { "a", "b" }));
|
||||||
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public void Concurrent_first_calls_are_thread_safe()
|
||||||
|
{
|
||||||
|
var failures = 0;
|
||||||
|
|
||||||
|
Parallel.For(0, 500, i =>
|
||||||
|
{
|
||||||
|
var expected = $"n{i}";
|
||||||
|
var actual = "[NAME]".ReplacePlaceholders(new ConcurrentModel { Name = expected });
|
||||||
|
|
||||||
|
if (actual != expected)
|
||||||
|
Interlocked.Increment(ref failures);
|
||||||
|
});
|
||||||
|
|
||||||
|
Assert.That(failures, Is.Zero);
|
||||||
|
}
|
||||||
|
|
||||||
|
#endregion
|
||||||
|
|
||||||
|
#region Real DTO
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public void Document_received_notification_is_fully_replaced()
|
||||||
|
{
|
||||||
|
var data = new DocumentReceivedNotification
|
||||||
|
{
|
||||||
|
DocumentTitle = "Vertrag",
|
||||||
|
NameReceiver = "Max",
|
||||||
|
NameSender = "Erika",
|
||||||
|
IsReadAndConfirm = true,
|
||||||
|
AdvancedSignature = false,
|
||||||
|
LinkToDocument = "https://x.io/a",
|
||||||
|
Message = "Bitte",
|
||||||
|
NamePortal = "Portal"
|
||||||
|
};
|
||||||
|
|
||||||
|
const string template =
|
||||||
|
"[NAME_RECEIVER]|[NAME_SENDER]|[SIGNATURE_TYPE]|[DOCUMENT_TITLE]|[MESSAGE]|[NAME_PORTAL]|[LINK_TO_DOCUMENT]|[LINK_TO_DOCUMENT_TEXT]";
|
||||||
|
|
||||||
|
var result = template.ReplacePlaceholders(data);
|
||||||
|
|
||||||
|
Assert.That(result, Is.EqualTo("Max|Erika|Signieren|Vertrag|Bitte|Portal|https://x.io/a|https://x.io/a"));
|
||||||
|
}
|
||||||
|
|
||||||
|
#endregion
|
||||||
|
|
||||||
|
#region Models
|
||||||
|
|
||||||
|
private sealed class PersonModel
|
||||||
|
{
|
||||||
|
public string FirstName { get; set; } = "v";
|
||||||
|
public string LastName { get; set; } = "v";
|
||||||
|
}
|
||||||
|
|
||||||
|
private sealed class PersonNameModel
|
||||||
|
{
|
||||||
|
public string Name { get; set; } = "v";
|
||||||
|
}
|
||||||
|
|
||||||
|
private sealed class IdModel
|
||||||
|
{
|
||||||
|
public string Id { get; set; } = "v";
|
||||||
|
}
|
||||||
|
|
||||||
|
private sealed class UpperIdModel
|
||||||
|
{
|
||||||
|
public string ID { get; set; } = "v";
|
||||||
|
}
|
||||||
|
|
||||||
|
private sealed class AcronymModel
|
||||||
|
{
|
||||||
|
public string URLValue { get; set; } = "v";
|
||||||
|
public string HTTPStatusCode { get; set; } = "v";
|
||||||
|
public string XMLHttpRequest { get; set; } = "v";
|
||||||
|
public string OTP { get; set; } = "v";
|
||||||
|
}
|
||||||
|
|
||||||
|
private sealed class DigitModel
|
||||||
|
{
|
||||||
|
public string Field1 { get; set; } = "v";
|
||||||
|
public string Value2Text { get; set; } = "v";
|
||||||
|
public string A1B2 { get; set; } = "v";
|
||||||
|
public string Name2 { get; set; } = "v";
|
||||||
|
}
|
||||||
|
|
||||||
|
private sealed class MyUrl2ValueModel
|
||||||
|
{
|
||||||
|
public string MyURL2Value { get; set; } = "v";
|
||||||
|
}
|
||||||
|
|
||||||
|
private sealed class SingleLetterModel
|
||||||
|
{
|
||||||
|
public string X { get; set; } = "v";
|
||||||
|
}
|
||||||
|
|
||||||
|
private sealed class LowerCaseModel
|
||||||
|
{
|
||||||
|
public string firstName { get; set; } = "v";
|
||||||
|
}
|
||||||
|
|
||||||
|
private sealed class LowerCaseInfoModel
|
||||||
|
{
|
||||||
|
public string info { get; set; } = "v";
|
||||||
|
}
|
||||||
|
|
||||||
|
private sealed class SnakeCaseModel
|
||||||
|
{
|
||||||
|
public string First_Name { get; set; } = "v";
|
||||||
|
}
|
||||||
|
|
||||||
|
private sealed class TfaModel
|
||||||
|
{
|
||||||
|
public string TFAExpiration { get; set; } = "v";
|
||||||
|
}
|
||||||
|
|
||||||
|
private sealed class CustomValue
|
||||||
|
{
|
||||||
|
public override string ToString() => "custom";
|
||||||
|
}
|
||||||
|
|
||||||
|
private sealed class EchoNameModel
|
||||||
|
{
|
||||||
|
public string Name { get; set; } = "[NAME]";
|
||||||
|
}
|
||||||
|
|
||||||
|
private sealed class DiscoveryModel
|
||||||
|
{
|
||||||
|
public static string Static { get; set; } = "static";
|
||||||
|
|
||||||
|
public string ReadOnly => "computed";
|
||||||
|
|
||||||
|
public string WriteOnly
|
||||||
|
{
|
||||||
|
set { }
|
||||||
|
}
|
||||||
|
|
||||||
|
public string this[int index] => "indexer";
|
||||||
|
|
||||||
|
public string Shown { get; set; } = "shown";
|
||||||
|
|
||||||
|
private string Hidden { get; set; } = "hidden";
|
||||||
|
}
|
||||||
|
|
||||||
|
private class BaseModel
|
||||||
|
{
|
||||||
|
public string Id { get; set; } = "base";
|
||||||
|
}
|
||||||
|
|
||||||
|
private sealed class DerivedModel : BaseModel
|
||||||
|
{
|
||||||
|
public string Name { get; set; } = "derived";
|
||||||
|
}
|
||||||
|
|
||||||
|
private sealed record PersonRecord(string FirstName, int Age);
|
||||||
|
|
||||||
|
private struct StructModel
|
||||||
|
{
|
||||||
|
public string Name { get; set; }
|
||||||
|
}
|
||||||
|
|
||||||
|
private sealed class Box<T>
|
||||||
|
{
|
||||||
|
public T Value { get; set; } = default!;
|
||||||
|
}
|
||||||
|
|
||||||
|
private sealed class TypeA
|
||||||
|
{
|
||||||
|
public string Name { get; set; } = "a";
|
||||||
|
}
|
||||||
|
|
||||||
|
private sealed class TypeB
|
||||||
|
{
|
||||||
|
public string Name { get; set; } = "b";
|
||||||
|
}
|
||||||
|
|
||||||
|
private sealed class ConcurrentModel
|
||||||
|
{
|
||||||
|
public string Name { get; set; } = string.Empty;
|
||||||
|
}
|
||||||
|
|
||||||
|
#endregion
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user