diff --git a/EnvelopeGenerator.Application/Common/Notifications/DocSigned/Handlers/DocStatusHandler.cs b/EnvelopeGenerator.Application/Common/Notifications/DocSigned/Handlers/DocStatusHandler.cs index adae66cf..dd90d3c5 100644 --- a/EnvelopeGenerator.Application/Common/Notifications/DocSigned/Handlers/DocStatusHandler.cs +++ b/EnvelopeGenerator.Application/Common/Notifications/DocSigned/Handlers/DocStatusHandler.cs @@ -29,15 +29,12 @@ public class DocStatusHandler : INotificationHandler /// /// /// - public async Task Handle(DocSignedNotification notification, CancellationToken cancel) + public Task Handle(DocSignedNotification notification, CancellationToken cancel) => _sender.Send(new CreateDocStatusCommand() { - await _sender.Send(new SaveDocStatusCommand() - { - Envelope = new() { Id = notification.EnvelopeId }, - Receiver = new() { Id = notification.ReceiverId}, - Value = notification.PsPdfKitAnnotation is PsPdfKitAnnotation annot - ? JsonSerializer.Serialize(annot.Instant, Format.Json.ForAnnotations) + EnvelopeId = notification.EnvelopeId, + ReceiverId = notification.ReceiverId, + Value = notification.PsPdfKitAnnotation is PsPdfKitAnnotation annot + ? JsonSerializer.Serialize(annot.Instant, Format.Json.ForAnnotations) : BlankAnnotationJson - }, cancel); - } + }, cancel); } \ No newline at end of file diff --git a/EnvelopeGenerator.Application/DocStatus/Commands/CreateDocStatusCommand.cs b/EnvelopeGenerator.Application/DocStatus/Commands/CreateDocStatusCommand.cs index 01b5d0a9..e8f78240 100644 --- a/EnvelopeGenerator.Application/DocStatus/Commands/CreateDocStatusCommand.cs +++ b/EnvelopeGenerator.Application/DocStatus/Commands/CreateDocStatusCommand.cs @@ -8,12 +8,22 @@ namespace EnvelopeGenerator.Application.DocStatus.Commands; /// /// /// -public record CreateDocStatusCommand : ModifyDocStatusCommandBase, IRequest +public record CreateDocStatusCommand : IRequest { /// - /// Gets timestamp when this record was added. Returns the StatusChangedWhen value. + /// /// - public DateTime AddedWhen => StatusChangedWhen; + public int EnvelopeId { get; set; } + + /// + /// + /// + public int ReceiverId { get; set; } + + /// + /// Gets or sets the display value associated with the status. + /// + public string? Value { get; set; } } /// diff --git a/EnvelopeGenerator.Application/DocStatus/Commands/ModifyDocStatusCommandBase.cs b/EnvelopeGenerator.Application/DocStatus/Commands/ModifyDocStatusCommandBase.cs deleted file mode 100644 index 259cb500..00000000 --- a/EnvelopeGenerator.Application/DocStatus/Commands/ModifyDocStatusCommandBase.cs +++ /dev/null @@ -1,54 +0,0 @@ -using EnvelopeGenerator.Application.Common.Query; -using EnvelopeGenerator.Domain.Constants; - -namespace EnvelopeGenerator.Application.DocStatus.Commands; - -/// -/// -/// -public record ModifyDocStatusCommandBase : EnvelopeReceiverQueryBase -{ - /// - /// - /// - public int? EnvelopeId => Envelope.Id; - - /// - /// - /// - public int? ReceiverId => Receiver.Id; - - /// - /// - /// - public override ReceiverQueryBase Receiver { get => base.Receiver; set => base.Receiver = value; } - - /// - /// Gets the current status code. - /// - public DocumentStatus Status => Value is null ? DocumentStatus.Created : DocumentStatus.Signed; - - /// - /// Gets or sets the display value associated with the status. - /// - public string? Value { get; set; } - - /// - /// Gets timestamp when this record was added. - /// - public DateTime StatusChangedWhen { get; } = DateTime.Now; - - /// - /// Maps the current command to a new instance of the specified type. - /// - /// - /// - public TDest To() where TDest : ModifyDocStatusCommandBase, new() - => new() - { - Key = Key, - Envelope = Envelope, - Receiver = Receiver, - Value = Value - }; -} \ No newline at end of file diff --git a/EnvelopeGenerator.Application/DocStatus/Commands/SaveDocStatusCommand.cs b/EnvelopeGenerator.Application/DocStatus/Commands/SaveDocStatusCommand.cs deleted file mode 100644 index 0f041e8a..00000000 --- a/EnvelopeGenerator.Application/DocStatus/Commands/SaveDocStatusCommand.cs +++ /dev/null @@ -1,77 +0,0 @@ -using DigitalData.Core.Abstraction.Application.Repository; -using EnvelopeGenerator.Domain.Entities; -using MediatR; -using Microsoft.EntityFrameworkCore; -using AutoMapper; -using EnvelopeGenerator.Application.Common.Dto; -using EnvelopeGenerator.Application.Common.Extensions; - -namespace EnvelopeGenerator.Application.DocStatus.Commands; - -/// -/// Represents a command to save the status of a document, either by creating a new status or updating an existing one based on the provided envelope and receiver identifiers. -/// It returns the identifier of the saved document status. -/// -public record SaveDocStatusCommand : ModifyDocStatusCommandBase, IRequest; - -/// -/// -/// -public class SaveDocStatusCommandHandler : IRequestHandler -{ - private readonly IMapper _mapper; - - private readonly IRepository _repo; - - private readonly IRepository _envRepo; - - private readonly IRepository _rcvRepo; - - /// - /// - /// - /// - /// - /// - /// - public SaveDocStatusCommandHandler(IMapper mapper, IRepository repo, IRepository rcvRepo, IRepository envRepo) - { - _mapper = mapper; - _repo = repo; - _rcvRepo = rcvRepo; - _envRepo = envRepo; - } - - /// - /// - /// - /// - /// - /// - public async Task Handle(SaveDocStatusCommand request, CancellationToken cancel) - { - // ceck if exists - bool isExists = await _repo.ReadOnly().Where(request).AnyAsync(cancel); - - var env = await _envRepo.ReadOnly().Where(request.Envelope).FirstAsync(cancel); - var rcv = await _rcvRepo.ReadOnly().Where(request.Receiver).FirstAsync(cancel); - - request.Envelope.Id = env.Id; - request.Receiver.Id = rcv.Id; - - if (isExists) - { - var uReq = request.To(); - await _repo.UpdateAsync(uReq, q => q.Where(request), cancel); - } - else - { - var cReq = request.To(); - await _repo.CreateAsync(cReq, cancel); - } - - var docStatus = await _repo.ReadOnly().Where(request).SingleOrDefaultAsync(cancel); - - return _mapper.Map(docStatus); - } -} \ No newline at end of file diff --git a/EnvelopeGenerator.Application/DocStatus/Commands/UpdateDocStatusCommand.cs b/EnvelopeGenerator.Application/DocStatus/Commands/UpdateDocStatusCommand.cs index f6c3b57f..dcdd8b18 100644 --- a/EnvelopeGenerator.Application/DocStatus/Commands/UpdateDocStatusCommand.cs +++ b/EnvelopeGenerator.Application/DocStatus/Commands/UpdateDocStatusCommand.cs @@ -1,14 +1,41 @@ -using EnvelopeGenerator.Domain; +using EnvelopeGenerator.Application.Common.Commands; +using EnvelopeGenerator.Domain.Entities; +using System.Linq.Expressions; namespace EnvelopeGenerator.Application.DocStatus.Commands; /// /// /// -public record UpdateDocStatusCommand : ModifyDocStatusCommandBase +/// +public record DocStatusUpdateDto(string? Value); + +/// +/// +/// +public record UpdateDocStatusCommand : UpdateCommand { /// - /// Gets timestamp when this record was added. Returns the StatusChangedWhen value. + /// /// - public DateTime? ChangedWhen => StatusChangedWhen; + public int EnvelopeId { get; set; } + + /// + /// + /// + public int ReceiverId { get; set; } + + /// + /// Gets or sets the display value associated with the status. + /// + public string? Value { get; set; } + + /// + /// + /// + /// + public override Expression> BuildQueryExpression() + { + return ds => ds.EnvelopeId == EnvelopeId && ds.ReceiverId == ReceiverId; + } } \ No newline at end of file diff --git a/EnvelopeGenerator.Application/DocStatus/MappingProfile.cs b/EnvelopeGenerator.Application/DocStatus/MappingProfile.cs index e1474d7e..bfa86dd5 100644 --- a/EnvelopeGenerator.Application/DocStatus/MappingProfile.cs +++ b/EnvelopeGenerator.Application/DocStatus/MappingProfile.cs @@ -18,11 +18,16 @@ public class MappingProfile : Profile CreateMap() .ForMember(dest => dest.Envelope, opt => opt.Ignore()) .ForMember(dest => dest.Receiver, opt => opt.Ignore()) + .ForMember(dest => dest.Status, opt => opt.MapFrom( + src => src.Value == null + ? Domain.Constants.DocumentStatus.Created + : Domain.Constants.DocumentStatus.Signed)) .MapAddedWhen(); CreateMap() .ForMember(dest => dest.Envelope, opt => opt.Ignore()) .ForMember(dest => dest.Receiver, opt => opt.Ignore()) + .ForMember(dest => dest.StatusChangedWhen, opt => opt.MapFrom(src => DateTime.UtcNow)) .MapChangedWhen(); } } \ No newline at end of file diff --git a/EnvelopeGenerator.Application/Histories/Commands/CreateHistoryCommand.cs b/EnvelopeGenerator.Application/Histories/Commands/CreateHistoryCommand.cs index 4cd83f01..c8a53f59 100644 --- a/EnvelopeGenerator.Application/Histories/Commands/CreateHistoryCommand.cs +++ b/EnvelopeGenerator.Application/Histories/Commands/CreateHistoryCommand.cs @@ -34,7 +34,7 @@ public record CreateHistoryCommand : EnvelopeReceiverQueryBase, IRequest /// /// - public DateTime AddedWhen { get; } = DateTime.Now; + public DateTime AddedWhen { get; } = DateTime.UtcNow; /// /// diff --git a/EnvelopeGenerator.Application/Histories/MappingProfile.cs b/EnvelopeGenerator.Application/Histories/MappingProfile.cs index 6261d5db..71e8916f 100644 --- a/EnvelopeGenerator.Application/Histories/MappingProfile.cs +++ b/EnvelopeGenerator.Application/Histories/MappingProfile.cs @@ -1,4 +1,5 @@ using AutoMapper; +using EnvelopeGenerator.Application.Common.Extensions; using EnvelopeGenerator.Application.Histories.Commands; using EnvelopeGenerator.Domain.Entities; @@ -17,6 +18,7 @@ public class MappingProfile: Profile CreateMap() .ForMember(dest => dest.Envelope, opt => opt.Ignore()) .ForMember(dest => dest.Sender, opt => opt.Ignore()) - .ForMember(dest => dest.Receiver, opt => opt.Ignore()); + .ForMember(dest => dest.Receiver, opt => opt.Ignore()) + .MapAddedWhen(); } } \ No newline at end of file diff --git a/EnvelopeGenerator.Application/Resources/Resource.cs b/EnvelopeGenerator.Application/Resources/Resource.cs index 53c11359..13223106 100644 --- a/EnvelopeGenerator.Application/Resources/Resource.cs +++ b/EnvelopeGenerator.Application/Resources/Resource.cs @@ -190,6 +190,13 @@ public static class Extensions /// public static string SignDoc(this IStringLocalizer localizer) => localizer[nameof(SignDoc)].Value; + /// + /// + /// + /// + /// + public static string ConfirmDoc(this IStringLocalizer localizer) => localizer[nameof(ConfirmDoc)].Value; + /// /// /// @@ -204,6 +211,13 @@ public static class Extensions /// public static string DocSigned(this IStringLocalizer localizer) => localizer[nameof(DocSigned)].Value; + /// + /// + /// + /// + /// + public static string DocConfirmed(this IStringLocalizer localizer) => localizer[nameof(DocConfirmed)].Value; + /// /// /// @@ -239,6 +253,13 @@ public static class Extensions /// public static string SigAgree(this IStringLocalizer localizer) => localizer[nameof(SigAgree)].Value; + /// + /// + /// + /// + /// + public static string ConfirmAgree(this IStringLocalizer localizer) => localizer[nameof(ConfirmAgree)].Value; + /// /// /// @@ -267,6 +288,13 @@ public static class Extensions /// public static string RejectionInfo1(this IStringLocalizer localizer) => localizer[nameof(RejectionInfo1)].Value; + /// + /// + /// + /// + /// + public static string RejectionInfo1ForConfirmation(this IStringLocalizer localizer) => localizer[nameof(RejectionInfo1ForConfirmation)].Value; + /// /// /// @@ -295,6 +323,13 @@ public static class Extensions /// public static string SigningProcessTitle(this IStringLocalizer localizer) => localizer[nameof(SigningProcessTitle)].Value; + /// + /// + /// + /// + /// + public static string ConfirmationProcessTitle(this IStringLocalizer localizer) => localizer[nameof(ConfirmationProcessTitle)].Value; + /// /// /// diff --git a/EnvelopeGenerator.Application/Resources/Resource.de-DE.resx b/EnvelopeGenerator.Application/Resources/Resource.de-DE.resx index e16727d0..16fb00f5 100644 --- a/EnvelopeGenerator.Application/Resources/Resource.de-DE.resx +++ b/EnvelopeGenerator.Application/Resources/Resource.de-DE.resx @@ -250,7 +250,7 @@ Sie können bei Bedarf mit {0}, <a href="mailto:{1}?subject={2}&body=Sehr geehrte(r)%20{0},%0A%0A%0A">{1}</a> Kontakt aufnehmen. - Das Vorgang wurde von einer der beteiligten Parteien abgelehnt. Sie können bei Bedarf mit {0}, <a href="mailto:{1}?subject={2}&body=Sehr geehrte(r)%20{0},%0A%0A%0A">{1}</a> Kontakt aufnehmen. + Der Vorgang wurde von einer der beteiligten Parteien abgelehnt. Sie können bei Bedarf mit {0}, <a href="mailto:{1}?subject={2}&body=Sehr geehrte(r)%20{0},%0A%0A%0A">{1}</a> Kontakt aufnehmen. Bitte geben Sie einen Grund an: @@ -447,4 +447,34 @@ Dokument wurde zurückgesetzt. + + Dokument erfolgreich gelesen und bestätigt! + + + Sie haben das Dokument gelesen und bestätigt. Im Anschluss erhalten Sie eine schriftliche Bestätigung. + + + Bestätigen + + + Dieser Bestätigungsvorgang wurde abgelehnt! + + + Dokument bestätigen + + + Dokument bestätigt + + + Durch Klick auf Abschließen bestätige ich, das Dokument gelesen und zur Kenntnis genommen zu haben. + + + Bestätigt von + + + Titel des Lesebetätigungs-Vorgangs + + + Bestätigungen + \ No newline at end of file diff --git a/EnvelopeGenerator.Application/Resources/Resource.en-US.resx b/EnvelopeGenerator.Application/Resources/Resource.en-US.resx index e12eb3bc..0e3a1af7 100644 --- a/EnvelopeGenerator.Application/Resources/Resource.en-US.resx +++ b/EnvelopeGenerator.Application/Resources/Resource.en-US.resx @@ -447,4 +447,34 @@ Document has been reset. + + Document successfully red and confirmed! + + + You have read and confirmed the document. You will receive a written confirmation afterwards. + + + Confirm + + + This confirmation process has been rejected! + + + Confirm Document + + + Document confirmed + + + By clicking on “Complete”, I confirm that I have read and taken note of the document. + + + Confirmed by + + + Title of the read confirmation process + + + Confirmations + \ No newline at end of file diff --git a/EnvelopeGenerator.Application/Resources/Resource.fr-FR.resx b/EnvelopeGenerator.Application/Resources/Resource.fr-FR.resx index 766e4fb9..be60d7bb 100644 --- a/EnvelopeGenerator.Application/Resources/Resource.fr-FR.resx +++ b/EnvelopeGenerator.Application/Resources/Resource.fr-FR.resx @@ -447,4 +447,34 @@ Le document a été réinitialisé. + + Document lu et confirmé avec succès ! + + + Vous avez lu et confirmé le document. Vous recevrez une confirmation écrite par la suite. + + + Confirmer + + + Cette procédure de confirmation a été rejetée ! + + + Confirmer le document + + + Document confirmé + + + En cliquant sur « Terminer », je confirme avoir lu et pris connaissance du document. + + + Confirmé par + + + Titre de la procédure de confirmation de lecture + + + Confirmations + \ No newline at end of file diff --git a/EnvelopeGenerator.Domain/Entities/DocumentStatus.cs b/EnvelopeGenerator.Domain/Entities/DocumentStatus.cs index c61bb4cd..3bec79c7 100644 --- a/EnvelopeGenerator.Domain/Entities/DocumentStatus.cs +++ b/EnvelopeGenerator.Domain/Entities/DocumentStatus.cs @@ -35,6 +35,10 @@ namespace EnvelopeGenerator.Domain.Entities [Column("STATUS")] public Constants.DocumentStatus Status { get; set; } + [Required] + [Column("STATUS_CHANGED_WHEN", TypeName = "datetime")] + public DateTime? StatusChangedWhen { get; set; } + [Required] [Column("ADDED_WHEN", TypeName = "datetime")] public DateTime AddedWhen { get; set; } diff --git a/EnvelopeGenerator.Web/Controllers/EnvelopeController.cs b/EnvelopeGenerator.Web/Controllers/EnvelopeController.cs index fed685c4..4bc9692e 100644 --- a/EnvelopeGenerator.Web/Controllers/EnvelopeController.cs +++ b/EnvelopeGenerator.Web/Controllers/EnvelopeController.cs @@ -91,7 +91,7 @@ public class EnvelopeController : ViewControllerBase if (await _historyService.IsSigned(envelopeId: er.Envelope!.Id, userReference: er.Receiver!.EmailAddress)) { await HttpContext.SignOutAsync(CookieAuthenticationDefaults.AuthenticationScheme); - return View("EnvelopeSigned"); + return View("EnvelopeSigned", er); } #endregion diff --git a/EnvelopeGenerator.Web/EnvelopeGenerator.Web.csproj b/EnvelopeGenerator.Web/EnvelopeGenerator.Web.csproj index 4612b69b..f2d2c9fe 100644 --- a/EnvelopeGenerator.Web/EnvelopeGenerator.Web.csproj +++ b/EnvelopeGenerator.Web/EnvelopeGenerator.Web.csproj @@ -12,9 +12,9 @@ digital data envelope generator web EnvelopeGenerator.Web is an ASP.NET MVC application developed to manage signing processes. It uses Entity Framework Core (EF Core) for database operations. The user interface for signing processes is developed with Razor View Engine (.cshtml files) and JavaScript under wwwroot, integrated with PSPDFKit. This integration allows users to view and sign documents seamlessly. Assets\icon.ico - 3.11.0 - 3.11.0.0 - 3.11.0.0 + 3.12.0 + 3.12.0.0 + 3.12.0.0 Copyright © 2025 Digital Data GmbH. All rights reserved. diff --git a/EnvelopeGenerator.Web/Views/Envelope/EnvelopeRejected.cshtml b/EnvelopeGenerator.Web/Views/Envelope/EnvelopeRejected.cshtml index 218dd2c0..2ee0a73b 100644 --- a/EnvelopeGenerator.Web/Views/Envelope/EnvelopeRejected.cshtml +++ b/EnvelopeGenerator.Web/Views/Envelope/EnvelopeRejected.cshtml @@ -9,12 +9,14 @@ @using EnvelopeGenerator.Web.Extensions @using Newtonsoft.Json @using Newtonsoft.Json.Serialization +@using EnvelopeGenerator.Domain.Interfaces; @model EnvelopeReceiverDto; @{ var envelope = Model.Envelope; var document = Model.Envelope?.Documents?.FirstOrDefault(); var sender = Model.Envelope?.User; var isExt = ViewBag.IsExt ?? false; + bool IsReadAndConfirm = Model!.Envelope!.IsReadAndConfirm(); }
@@ -54,7 +56,11 @@ c-5.791,5.79-15.176,5.79-20.969,0l-30.32-30.322l-11.676,11.676l30.32,30.32c5.79,5.79,5.79,15.178,0,20.969L299.11,404.045z"/>
-

@(isExt ? _localizer.RejectionInfo1Ext() : _localizer.RejectionInfo1())

+

@(isExt + ? _localizer.RejectionInfo1Ext() + : IsReadAndConfirm + ? _localizer.RejectionInfo1ForConfirmation() + : _localizer.RejectionInfo1())

diff --git a/EnvelopeGenerator.Web/Views/Envelope/EnvelopeSigned.cshtml b/EnvelopeGenerator.Web/Views/Envelope/EnvelopeSigned.cshtml index e2c5ae4d..0aed7cf2 100644 --- a/EnvelopeGenerator.Web/Views/Envelope/EnvelopeSigned.cshtml +++ b/EnvelopeGenerator.Web/Views/Envelope/EnvelopeSigned.cshtml @@ -1,5 +1,11 @@ @{ - ViewData["Title"] = _localizer.DocSigned(); + @using EnvelopeGenerator.Application.Common.Dto.EnvelopeReceiver; + @using EnvelopeGenerator.Domain.Interfaces; + @model EnvelopeReceiverDto; + bool IsReadAndConfirm = Model!.Envelope!.IsReadAndConfirm(); + ViewData["Title"] = IsReadAndConfirm + ? _localizer.DocConfirmed() + : _localizer.DocSigned(); }
@@ -9,9 +15,13 @@
-

@_localizer["DocumentSuccessfullySigned"]

+

@(IsReadAndConfirm + ? _localizer["DocumentSuccessfullyConfirmed"] + : _localizer["DocumentSuccessfullySigned"])

-

@_localizer["DocumentSignedConfirmationMessage"]

+

@(IsReadAndConfirm + ? _localizer["DocumentConfirmedConfirmationMessage"] + : _localizer["DocumentSignedConfirmationMessage"])

\ No newline at end of file diff --git a/EnvelopeGenerator.Web/Views/Envelope/ShowEnvelope.cshtml b/EnvelopeGenerator.Web/Views/Envelope/ShowEnvelope.cshtml index b710f772..8a454993 100644 --- a/EnvelopeGenerator.Web/Views/Envelope/ShowEnvelope.cshtml +++ b/EnvelopeGenerator.Web/Views/Envelope/ShowEnvelope.cshtml @@ -9,6 +9,7 @@ @using EnvelopeGenerator.Web.Extensions @using Newtonsoft.Json @using Newtonsoft.Json.Serialization +@using EnvelopeGenerator.Domain.Interfaces @model EnvelopeReceiverDto; @{ var userCulture = ViewData["UserCulture"] as Culture; @@ -24,7 +25,13 @@ if (ViewData["IsReadOnly"] is bool isReadOnly_bool) isReadOnly = isReadOnly_bool; - ViewData["Title"] = isReadOnly ? _localizer.ViewDoc() : _localizer.SignDoc(); + var isReadAndConfirm = envelope.IsReadAndConfirm(); + + ViewData["Title"] = isReadOnly + ? _localizer.ViewDoc() + : isReadAndConfirm + ? _localizer.ConfirmDoc() + : _localizer.SignDoc(); }
@if (!isReadOnly) @@ -62,7 +69,9 @@
- 0/@signatureCount @_localizer["Signatures"] + 0/@signatureCount @(isReadAndConfirm + ? _localizer["Confirmations"] + : _localizer["Signatures"])
}