Compare commits
31 Commits
feat/custo
...
bugfix/sav
| Author | SHA1 | Date | |
|---|---|---|---|
| d0c3d79fbc | |||
| e81c7896a1 | |||
| 127e2b253e | |||
| 6d0b78072e | |||
| 3721ea9a1f | |||
| f86588551c | |||
| 0eaa9ff08f | |||
| ec79d7622e | |||
| f558d03bdf | |||
| db79053c62 | |||
| 85dd85aa92 | |||
| c7bc9fbcca | |||
| 1e8e5c96c4 | |||
| 61d04ca51a | |||
| af674a811d | |||
| 123889451f | |||
| 3b5075fe44 | |||
| 48302a374f | |||
| b2c245d6bd | |||
| e9c0ec139a | |||
| 819bdcfcfb | |||
| ad7686b7c2 | |||
| ade542ded0 | |||
| 01c1283622 | |||
| d969dbe843 | |||
| 41779a4f58 | |||
| 618451f0c1 | |||
| 96c7cd6e8a | |||
| 4047ba7c6b | |||
| 897860b2bf | |||
| 8933cff558 |
15
AGENTS.md
15
AGENTS.md
@@ -236,6 +236,21 @@ Manual testing workflow:
|
|||||||
3. Test sender login at `/sender/login`
|
3. Test sender login at `/sender/login`
|
||||||
4. Test receiver flow at `/envelope/login/{envelopeKey}`
|
4. Test receiver flow at `/envelope/login/{envelopeKey}`
|
||||||
|
|
||||||
|
## Technical Debt TODOs
|
||||||
|
|
||||||
|
- Refactor `EnvelopeGenerator.Server/EnvelopeGenerator.Server/Components/Pages/EnvelopeSenderPage.razor` to reduce complexity.
|
||||||
|
- Move sender dashboard business/status logic into dedicated services and extension methods (for example effective status resolution, tab classification, receiver signed/rejected detection, and grid-layout filter sanitation).
|
||||||
|
- Keep `EnvelopeSenderPage.razor` focused on UI composition/state orchestration; avoid embedding heavy domain logic directly in the Razor component.
|
||||||
|
- Apply a modular page architecture across Server pages (starting with `EnvelopeGenerator.Server/EnvelopeGenerator.Server/Components/Pages/EnvelopeSenderPage.razor`): split large pages into smaller subcomponents/partial modules, eliminate duplicated code paths, and centralize shared UI/domain logic for maintainability.
|
||||||
|
- Perform a senior-level page performance review across sender/receiver flows before further UX changes: verify route transitions, component mount/unmount behavior, loading-state correctness, duplicate API calls, and perceived latency (first meaningful paint and return-navigation responsiveness). Treat loading logic defects as architecture/performance issues first, not styling problems.
|
||||||
|
- Audit `EnvelopeGenerator.Server/EnvelopeGenerator.Server.Client/Services/` for unnecessary request/payload remapping and remove redundant mappings where direct request DTO usage is safe; review client HTTP methods end-to-end for similar avoidable transformation layers.
|
||||||
|
- Centralize dependency injection registrations into dedicated extension methods/modules (especially server + client service registrations in `Program.cs`) to reduce startup composition sprawl and improve maintainability.
|
||||||
|
- Refactor `EnvelopeGenerator.Server/EnvelopeGenerator.Server/Controllers/PdfRenderController.cs` + matching client service contract to remove JSON/base64 `byte[]` transport for PDF rendering; prefer binary streaming (`application/octet-stream` or multipart), avoid `data:` URL bloat where possible, and validate end-to-end memory/latency behavior with large PDFs.
|
||||||
|
- Establish a DevExpress toast-based notification architecture using `DxToastProvider` + `IToastNotificationService` (tab/page-scoped provider strategy, standardized `ToastOptions` factory, and severity-to-style mapping) so sender/receiver flows show consistent, non-duplicated, actionable feedback.
|
||||||
|
- Eliminate manual property-by-property request→domain mapping drift (example: `CreateEnvelopeCommandHandler`) by introducing a standardized mapping policy (AutoMapper/profile conventions or reflection/source-generator based mapper) and enforcing it across envelope workflows; this needs to be solved in the shared company Core library level so all services get the same guardrails by default.
|
||||||
|
- P1 security workstream: remove dynamic SQL string composition/interpolation from active save/create/update paths (`string.Format`, `$"...{input}..."`, `ToSqlParam`) and migrate to parameterized queries only; treat this as a potential SQL injection vulnerability (stability + security risk), execute repo-wide audit, and add CI guardrails to block reintroduction.
|
||||||
|
- Alternative UX design (Server component): in `EnvelopeGenerator.Server/EnvelopeGenerator.Server/Components/Pages/EnvelopeSenderPage.razor`, enforce deterministic row double-click behavior as exactly one action (preview OR edit), and keep row action buttons/tooltips aligned with the same rule to eliminate ambiguous navigation.
|
||||||
|
|
||||||
## Database
|
## Database
|
||||||
|
|
||||||
**SQL Server** (DD_ECM)
|
**SQL Server** (DD_ECM)
|
||||||
|
|||||||
@@ -0,0 +1,40 @@
|
|||||||
|
using DigitalData.Core.Exceptions;
|
||||||
|
using FluentValidation;
|
||||||
|
using MediatR;
|
||||||
|
|
||||||
|
namespace EnvelopeGenerator.Application.Common.Behaviors;
|
||||||
|
|
||||||
|
public class ValidationBehavior<TRequest, TResponse> : IPipelineBehavior<TRequest, TResponse>
|
||||||
|
where TRequest : notnull
|
||||||
|
{
|
||||||
|
private readonly IEnumerable<IValidator<TRequest>> _validators;
|
||||||
|
|
||||||
|
public ValidationBehavior(IEnumerable<IValidator<TRequest>> validators)
|
||||||
|
{
|
||||||
|
_validators = validators;
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<TResponse> Handle(
|
||||||
|
TRequest request,
|
||||||
|
RequestHandlerDelegate<TResponse> next,
|
||||||
|
CancellationToken cancellationToken)
|
||||||
|
{
|
||||||
|
if (_validators.Any())
|
||||||
|
{
|
||||||
|
var context = new ValidationContext<TRequest>(request);
|
||||||
|
var failures = _validators
|
||||||
|
.Select(v => v.Validate(context))
|
||||||
|
.SelectMany(r => r.Errors)
|
||||||
|
.Where(f => f is not null)
|
||||||
|
.ToList();
|
||||||
|
|
||||||
|
if (failures.Count > 0)
|
||||||
|
{
|
||||||
|
var message = string.Join("; ", failures.Select(f => f.ErrorMessage).Distinct());
|
||||||
|
throw new BadRequestException(message);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return await next();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,27 @@
|
|||||||
|
namespace EnvelopeGenerator.Application.Common.Dto;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Data Transfer Object representing document metadata and elements without binary payload.
|
||||||
|
/// </summary>
|
||||||
|
public record DocumentDetailsDto
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Gets or sets the unique identifier of the document.
|
||||||
|
/// </summary>
|
||||||
|
public int Id { get; init; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets or sets the envelope ID to which the document belongs.
|
||||||
|
/// </summary>
|
||||||
|
public int EnvelopeId { get; init; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets or sets the date and time when the document was added.
|
||||||
|
/// </summary>
|
||||||
|
public DateTime AddedWhen { get; init; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets or sets the collection of elements associated with the document for receiver interactions, if any.
|
||||||
|
/// </summary>
|
||||||
|
public IEnumerable<DocReceiverElementDto>? Elements { get; init; }
|
||||||
|
}
|
||||||
@@ -3,30 +3,25 @@
|
|||||||
/// <summary>
|
/// <summary>
|
||||||
/// Data Transfer Object representing a document within an envelope, including optional binary data and form elements.
|
/// Data Transfer Object representing a document within an envelope, including optional binary data and form elements.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public class DocumentDto
|
public record DocumentDto : DocumentDetailsDto
|
||||||
{
|
{
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Gets or sets the unique identifier of the document.
|
///
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public int Id { get; set; }
|
public DocumentDto() { }
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Gets or sets the envelope ID to which the document belongs.
|
///
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public int EnvelopeId { get; set; }
|
/// <param name="byteData"></param>
|
||||||
|
/// <param name="details"></param>
|
||||||
/// <summary>
|
public DocumentDto(byte[] byteData, DocumentDetailsDto details) : base(details)
|
||||||
/// Gets or sets the date and time when the document was added.
|
{
|
||||||
/// </summary>
|
ByteData = byteData;
|
||||||
public DateTime AddedWhen { get; set; }
|
}
|
||||||
|
|
||||||
/// <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; set; }
|
public byte[]? ByteData { get; init; }
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Gets or sets the collection of elements associated with the document for receiver interactions, if any.
|
|
||||||
/// </summary>
|
|
||||||
public IEnumerable<DocReceiverElementDto>? Elements { get; set; }
|
|
||||||
}
|
}
|
||||||
@@ -86,6 +86,41 @@ public record EnvelopeDto : IEnvelope
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
public int? CertificationType { get; set; }
|
public int? CertificationType { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
///
|
||||||
|
/// </summary>
|
||||||
|
public int? FinalEmailToCreator { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
///
|
||||||
|
/// </summary>
|
||||||
|
public int? FinalEmailToReceivers { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
///
|
||||||
|
/// </summary>
|
||||||
|
public bool? SendReminderEmails { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
///
|
||||||
|
/// </summary>
|
||||||
|
public int? FirstReminderDays { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
///
|
||||||
|
/// </summary>
|
||||||
|
public int? ReminderIntervalDays { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
///
|
||||||
|
/// </summary>
|
||||||
|
public int? ExpiresWhenDays { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
///
|
||||||
|
/// </summary>
|
||||||
|
public int? ExpiresWarningWhenDays { get; set; }
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
///
|
///
|
||||||
/// </summary>
|
/// </summary>
|
||||||
|
|||||||
@@ -32,6 +32,7 @@ public class MappingProfile : Profile
|
|||||||
.ForMember(dest => dest.EnvelopeTypeTitle,
|
.ForMember(dest => dest.EnvelopeTypeTitle,
|
||||||
opt => opt.MapFrom(src => src.Type != null ? src.Type.Title : null));
|
opt => opt.MapFrom(src => src.Type != null ? src.Type.Title : null));
|
||||||
CreateMap<Document, DocumentDto>();
|
CreateMap<Document, DocumentDto>();
|
||||||
|
CreateMap<Document, DocumentDetailsDto>();
|
||||||
CreateMap<History, HistoryDto>().ForMember(dest => dest.ActionDate, opt => opt.MapFrom(src => src.ChangedWhen));
|
CreateMap<History, HistoryDto>().ForMember(dest => dest.ActionDate, opt => opt.MapFrom(src => src.ChangedWhen));
|
||||||
CreateMap<History, HistoryCreateDto>().ForMember(dest => dest.ActionDate, opt => opt.MapFrom(src => src.ChangedWhen));
|
CreateMap<History, HistoryCreateDto>().ForMember(dest => dest.ActionDate, opt => opt.MapFrom(src => src.ChangedWhen));
|
||||||
CreateMap<EnvelopeReceiver, EnvelopeReceiverDto>();
|
CreateMap<EnvelopeReceiver, EnvelopeReceiverDto>();
|
||||||
|
|||||||
@@ -10,6 +10,8 @@ using System.Reflection;
|
|||||||
using MediatR;
|
using MediatR;
|
||||||
using EnvelopeGenerator.Application.DocReceiverElements.Commands;
|
using EnvelopeGenerator.Application.DocReceiverElements.Commands;
|
||||||
using EnvelopeGenerator.Application.DocReceiverElements.Behaviors;
|
using EnvelopeGenerator.Application.DocReceiverElements.Behaviors;
|
||||||
|
using EnvelopeGenerator.Application.Common.Behaviors;
|
||||||
|
using FluentValidation;
|
||||||
|
|
||||||
namespace EnvelopeGenerator.Application;
|
namespace EnvelopeGenerator.Application;
|
||||||
|
|
||||||
@@ -60,6 +62,9 @@ public static class DependencyInjection
|
|||||||
{
|
{
|
||||||
cfg.RegisterServicesFromAssembly(Assembly.GetExecutingAssembly());
|
cfg.RegisterServicesFromAssembly(Assembly.GetExecutingAssembly());
|
||||||
|
|
||||||
|
// Generic request validation
|
||||||
|
cfg.AddOpenBehavior(typeof(ValidationBehavior<,>));
|
||||||
|
|
||||||
// Register SignCommand pipeline behaviors in execution order
|
// Register SignCommand pipeline behaviors in execution order
|
||||||
// 0. EnvelopeReceiverResolutionBehavior - Resolves EnvelopeReceiver from query parameters (executes FIRST)
|
// 0. EnvelopeReceiverResolutionBehavior - Resolves EnvelopeReceiver from query parameters (executes FIRST)
|
||||||
cfg.AddBehavior<IPipelineBehavior<SigningCommand, Unit>, EnvelopeReceiverResolutionBehavior>();
|
cfg.AddBehavior<IPipelineBehavior<SigningCommand, Unit>, EnvelopeReceiverResolutionBehavior>();
|
||||||
@@ -80,6 +85,8 @@ public static class DependencyInjection
|
|||||||
cfg.AddBehavior<IPipelineBehavior<SigningCommand, Unit>, SendSignedMailBehavior>();
|
cfg.AddBehavior<IPipelineBehavior<SigningCommand, Unit>, SendSignedMailBehavior>();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
services.AddValidatorsFromAssembly(Assembly.GetExecutingAssembly());
|
||||||
|
|
||||||
return services;
|
return services;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,44 @@
|
|||||||
|
using AutoMapper;
|
||||||
|
using AutoMapper.QueryableExtensions;
|
||||||
|
using DigitalData.Core.Abstraction.Application.Repository;
|
||||||
|
using EnvelopeGenerator.Application.Common.Dto;
|
||||||
|
using EnvelopeGenerator.Domain.Entities;
|
||||||
|
using MediatR;
|
||||||
|
using Microsoft.EntityFrameworkCore;
|
||||||
|
|
||||||
|
namespace EnvelopeGenerator.Application.Documents.Queries;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Represents a query to read document metadata/details without binary payload.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="EnvelopeId">The identifier of the envelope associated with the document.</param>
|
||||||
|
/// <param name="UserId">The unique identifier of the sending user. Optional.</param>
|
||||||
|
public record ReadDocumentDetailsQuery(int EnvelopeId, int? UserId = null) : IRequest<DocumentDetailsDto?>;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Handles queries for reading document details without loading BYTE_DATA.
|
||||||
|
/// </summary>
|
||||||
|
public class ReadDocumentDetailsQueryHandler : IRequestHandler<ReadDocumentDetailsQuery, DocumentDetailsDto?>
|
||||||
|
{
|
||||||
|
private readonly IRepository<Document> _repo;
|
||||||
|
private readonly IMapper _mapper;
|
||||||
|
|
||||||
|
public ReadDocumentDetailsQueryHandler(IRepository<Document> repo, IMapper mapper)
|
||||||
|
{
|
||||||
|
_repo = repo;
|
||||||
|
_mapper = mapper;
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<DocumentDetailsDto?> Handle(ReadDocumentDetailsQuery query, CancellationToken cancel)
|
||||||
|
{
|
||||||
|
var docQuery = _repo.Query.AsNoTracking();
|
||||||
|
|
||||||
|
if (query.UserId is int userId)
|
||||||
|
docQuery = docQuery.Where(doc => doc.Envelope.UserId == userId);
|
||||||
|
|
||||||
|
return await docQuery
|
||||||
|
.Where(d => d.EnvelopeId == query.EnvelopeId)
|
||||||
|
.ProjectTo<DocumentDetailsDto>(_mapper.ConfigurationProvider)
|
||||||
|
.FirstOrDefaultAsync(cancel);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,13 @@
|
|||||||
|
using FluentValidation;
|
||||||
|
|
||||||
|
namespace EnvelopeGenerator.Application.Documents.Queries;
|
||||||
|
|
||||||
|
public class ReadDocumentDetailsQueryValidator : AbstractValidator<ReadDocumentDetailsQuery>
|
||||||
|
{
|
||||||
|
public ReadDocumentDetailsQueryValidator()
|
||||||
|
{
|
||||||
|
RuleFor(x => x.EnvelopeId)
|
||||||
|
.GreaterThan(0)
|
||||||
|
.WithMessage("EnvelopeId is required.");
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,23 @@
|
|||||||
|
using FluentValidation;
|
||||||
|
|
||||||
|
namespace EnvelopeGenerator.Application.Documents.Queries;
|
||||||
|
|
||||||
|
public class ReadDocumentQueryValidator : AbstractValidator<ReadDocumentQuery>
|
||||||
|
{
|
||||||
|
public ReadDocumentQueryValidator()
|
||||||
|
{
|
||||||
|
RuleFor(x => x)
|
||||||
|
.Must(x => (x.Id ?? 0) > 0 || (x.EnvelopeId ?? 0) > 0)
|
||||||
|
.WithMessage("Id or EnvelopeId is required.");
|
||||||
|
|
||||||
|
RuleFor(x => x.Id)
|
||||||
|
.GreaterThan(0)
|
||||||
|
.When(x => x.Id.HasValue)
|
||||||
|
.WithMessage("Id must be greater than 0.");
|
||||||
|
|
||||||
|
RuleFor(x => x.EnvelopeId)
|
||||||
|
.GreaterThan(0)
|
||||||
|
.When(x => x.EnvelopeId.HasValue)
|
||||||
|
.WithMessage("EnvelopeId must be greater than 0.");
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -19,6 +19,8 @@
|
|||||||
<PackageReference Include="DigitalData.Core.Client" Version="2.1.0" />
|
<PackageReference Include="DigitalData.Core.Client" Version="2.1.0" />
|
||||||
<PackageReference Include="DigitalData.Core.Exceptions" Version="1.1.0" />
|
<PackageReference Include="DigitalData.Core.Exceptions" Version="1.1.0" />
|
||||||
<PackageReference Include="DigitalData.EmailProfilerDispatcher" Version="3.1.1" />
|
<PackageReference Include="DigitalData.EmailProfilerDispatcher" Version="3.1.1" />
|
||||||
|
<PackageReference Include="FluentValidation" Version="11.11.0" />
|
||||||
|
<PackageReference Include="FluentValidation.DependencyInjectionExtensions" Version="11.11.0" />
|
||||||
<PackageReference Include="HtmlSanitizer" Version="9.0.892" />
|
<PackageReference Include="HtmlSanitizer" Version="9.0.892" />
|
||||||
<PackageReference Include="MediatR" Version="12.5.0" />
|
<PackageReference Include="MediatR" Version="12.5.0" />
|
||||||
<PackageReference Include="Microsoft.Identity.Client" Version="4.82.1" />
|
<PackageReference Include="Microsoft.Identity.Client" Version="4.82.1" />
|
||||||
|
|||||||
@@ -1,6 +1,5 @@
|
|||||||
using EnvelopeGenerator.Application.Common.Dto;
|
using EnvelopeGenerator.Application.Common.Dto;
|
||||||
using MediatR;
|
using MediatR;
|
||||||
using AutoMapper;
|
|
||||||
using DigitalData.Core.Abstraction.Application.Repository;
|
using DigitalData.Core.Abstraction.Application.Repository;
|
||||||
using EnvelopeGenerator.Domain.Entities;
|
using EnvelopeGenerator.Domain.Entities;
|
||||||
using Microsoft.EntityFrameworkCore;
|
using Microsoft.EntityFrameworkCore;
|
||||||
@@ -18,17 +17,14 @@ public record ReadEnvelopeTypesQuery : IRequest<IEnumerable<EnvelopeTypeDto>>;
|
|||||||
public class ReadEnvelopeTypesQueryHandler : IRequestHandler<ReadEnvelopeTypesQuery, IEnumerable<EnvelopeTypeDto>>
|
public class ReadEnvelopeTypesQueryHandler : IRequestHandler<ReadEnvelopeTypesQuery, IEnumerable<EnvelopeTypeDto>>
|
||||||
{
|
{
|
||||||
private readonly IRepository<EnvelopeType> _repository;
|
private readonly IRepository<EnvelopeType> _repository;
|
||||||
private readonly IMapper _mapper;
|
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
///
|
///
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="repository"></param>
|
/// <param name="repository"></param>
|
||||||
/// <param name="mapper"></param>
|
public ReadEnvelopeTypesQueryHandler(IRepository<EnvelopeType> repository)
|
||||||
public ReadEnvelopeTypesQueryHandler(IRepository<EnvelopeType> repository, IMapper mapper)
|
|
||||||
{
|
{
|
||||||
_repository = repository;
|
_repository = repository;
|
||||||
_mapper = mapper;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
@@ -39,7 +35,28 @@ public class ReadEnvelopeTypesQueryHandler : IRequestHandler<ReadEnvelopeTypesQu
|
|||||||
/// <returns></returns>
|
/// <returns></returns>
|
||||||
public async Task<IEnumerable<EnvelopeTypeDto>> Handle(ReadEnvelopeTypesQuery request, CancellationToken cancellationToken)
|
public async Task<IEnumerable<EnvelopeTypeDto>> Handle(ReadEnvelopeTypesQuery request, CancellationToken cancellationToken)
|
||||||
{
|
{
|
||||||
var types = await _repository.Query.ToListAsync(cancellationToken);
|
// TODO: Temporary manual projection to avoid null-materialization crashes from legacy nullable DB columns in EnvelopeType.
|
||||||
return _mapper.Map<IEnumerable<EnvelopeTypeDto>>(types);
|
// Revisit and switch to AutoMapper query projection (ProjectTo) after schema nullability and mapping contracts are stabilized.
|
||||||
|
return await _repository.Query
|
||||||
|
.AsNoTracking()
|
||||||
|
.Select(t => new EnvelopeTypeDto
|
||||||
|
{
|
||||||
|
Id = t.Id,
|
||||||
|
Title = t.Title ?? string.Empty,
|
||||||
|
Language = string.IsNullOrWhiteSpace(t.Language) ? "de-DE" : t.Language,
|
||||||
|
ExpiresDays = t.ExpiresDays,
|
||||||
|
CertificationType = (int?)t.CertificationType,
|
||||||
|
UseAccessCode = t.UseAccessCode,
|
||||||
|
FinalEmailToCreator = (int?)t.FinalEmailToCreator,
|
||||||
|
FinalEmailToReceivers = (int?)t.FinalEmailToReceivers,
|
||||||
|
AddedWhen = t.AddedWhen,
|
||||||
|
ChangedWhen = t.ChangedWhen,
|
||||||
|
ExpiresWarningDays = t.ExpiresWarningDays,
|
||||||
|
SendReminderEmails = t.SendReminderEmails,
|
||||||
|
FirstReminderDays = t.FirstReminderDays,
|
||||||
|
ReminderIntervalDays = t.ReminderIntervalDays,
|
||||||
|
ContractType = t.ContractType
|
||||||
|
})
|
||||||
|
.ToListAsync(cancellationToken);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -12,8 +12,66 @@ namespace EnvelopeGenerator.Application.Envelopes.Commands;
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
public record CreateEnvelopeCommand : IRequest<EnvelopeDto?>
|
public record CreateEnvelopeCommand : IRequest<EnvelopeDto?>
|
||||||
{
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Optionale Umschlag-ID fuer Aktualisierungsvorgaenge.
|
||||||
|
/// </summary>
|
||||||
public int? EnvelopeId { get; init; }
|
public int? EnvelopeId { get; init; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Kennung des ausgewaehlten Umschlagtyps (Vorlage).
|
||||||
|
/// </summary>
|
||||||
|
public int? EnvelopeTypeId { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Sprachcode fuer Umschlaginhalt und Benachrichtigungen.
|
||||||
|
/// </summary>
|
||||||
|
public string? Language { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gibt an, ob der Zugriffscode fuer Empfaenger aktiviert ist.
|
||||||
|
/// </summary>
|
||||||
|
public bool? UseAccessCode { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Zertifizierungstyp fuer den Signaturprozess.
|
||||||
|
/// </summary>
|
||||||
|
public int? CertificationType { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Finale E-Mail-Einstellung fuer den Ersteller.
|
||||||
|
/// </summary>
|
||||||
|
public int? FinalEmailToCreator { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Finale E-Mail-Einstellung fuer die Empfaenger.
|
||||||
|
/// </summary>
|
||||||
|
public int? FinalEmailToReceivers { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gibt an, ob Erinnerungs-E-Mails aktiviert sind.
|
||||||
|
/// </summary>
|
||||||
|
public bool? SendReminderEmails { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Anzahl der Tage bis zur ersten Erinnerung.
|
||||||
|
/// </summary>
|
||||||
|
public int? FirstReminderDays { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Intervall in Tagen zwischen Erinnerungs-E-Mails.
|
||||||
|
/// </summary>
|
||||||
|
public int? ReminderIntervalDays { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Anzahl der Tage bis zum Ablauf des Umschlags.
|
||||||
|
/// </summary>
|
||||||
|
public int? ExpiresWhenDays { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Anzahl der Tage vor Ablauf, ab denen gewarnt wird.
|
||||||
|
/// </summary>
|
||||||
|
public int? ExpiresWarningWhenDays { get; set; }
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Der Titel des Umschlags. Dies ist ein Pflichtfeld.
|
/// Der Titel des Umschlags. Dies ist ein Pflichtfeld.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
@@ -29,10 +87,19 @@ public record CreateEnvelopeCommand : IRequest<EnvelopeDto?>
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
public bool TFAEnabled { get; set; } = false;
|
public bool TFAEnabled { get; set; } = false;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gibt an, ob der Umschlag nach dem Speichern sofort versendet werden soll.
|
||||||
|
/// </summary>
|
||||||
public bool Send { get; init; }
|
public bool Send { get; init; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Optionaler PDF-Dokumentinhalt als Base64.
|
||||||
|
/// </summary>
|
||||||
public CreateEnvelopeDocumentDto? Document { get; init; }
|
public CreateEnvelopeDocumentDto? Document { get; init; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Empfaengerliste inklusive optionaler Signaturfeld-Koordinaten.
|
||||||
|
/// </summary>
|
||||||
public List<CreateEnvelopeReceiverDto> Receivers { get; init; } = new();
|
public List<CreateEnvelopeReceiverDto> Receivers { get; init; } = new();
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
|
|||||||
@@ -33,6 +33,8 @@ public class CreateEnvelopeCommandHandler : IRequestHandler<CreateEnvelopeComman
|
|||||||
|
|
||||||
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<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>>();
|
||||||
@@ -73,6 +75,7 @@ public class CreateEnvelopeCommandHandler : IRequestHandler<CreateEnvelopeComman
|
|||||||
var now = DateTime.Now;
|
var now = DateTime.Now;
|
||||||
var title = request.Title?.Trim() ?? string.Empty;
|
var title = request.Title?.Trim() ?? string.Empty;
|
||||||
var message = request.Message?.Trim() ?? string.Empty;
|
var message = request.Message?.Trim() ?? string.Empty;
|
||||||
|
var settings = await ResolveEnvelopeSettingsAsync(request, existing: null, cancel);
|
||||||
|
|
||||||
Envelope envelope;
|
Envelope envelope;
|
||||||
if (request.EnvelopeId is int envelopeId)
|
if (request.EnvelopeId is int envelopeId)
|
||||||
@@ -85,11 +88,27 @@ public class CreateEnvelopeCommandHandler : IRequestHandler<CreateEnvelopeComman
|
|||||||
if (envelope.UserId != request.UserId)
|
if (envelope.UserId != request.UserId)
|
||||||
throw new ForbiddenException();
|
throw new ForbiddenException();
|
||||||
|
|
||||||
|
settings = await ResolveEnvelopeSettingsAsync(request, envelope, cancel);
|
||||||
|
|
||||||
await Repository.UpdateAsync(e =>
|
await Repository.UpdateAsync(e =>
|
||||||
{
|
{
|
||||||
e.Title = title;
|
e.Title = title;
|
||||||
e.Message = message;
|
e.Message = message;
|
||||||
e.TfaEnabled = request.TFAEnabled;
|
e.TfaEnabled = request.TFAEnabled;
|
||||||
|
e.EnvelopeTypeId = settings.EnvelopeTypeId;
|
||||||
|
e.Language = settings.Language;
|
||||||
|
e.UseAccessCode = settings.UseAccessCode;
|
||||||
|
e.CertificationType = settings.CertificationType;
|
||||||
|
e.FinalEmailToCreator = settings.FinalEmailToCreator;
|
||||||
|
e.FinalEmailToReceivers = settings.FinalEmailToReceivers;
|
||||||
|
e.SendReminderEmails = settings.SendReminderEmails;
|
||||||
|
e.FirstReminderDays = settings.FirstReminderDays;
|
||||||
|
e.ReminderIntervalDays = settings.ReminderIntervalDays;
|
||||||
|
e.ExpiresWhenDays = settings.ExpiresWhenDays;
|
||||||
|
e.ExpiresWarningWhenDays = settings.ExpiresWarningWhenDays;
|
||||||
|
e.ContractType = settings.ContractType;
|
||||||
|
e.ExpiresWhen = settings.ExpiresWhen;
|
||||||
|
e.ExpiresWarningWhen = settings.ExpiresWarningWhen;
|
||||||
e.Status = EnvelopeStatus.EnvelopeSaved;
|
e.Status = EnvelopeStatus.EnvelopeSaved;
|
||||||
e.ChangedWhen = now;
|
e.ChangedWhen = now;
|
||||||
}, e => e.Id == envelopeId, cancel);
|
}, e => e.Id == envelopeId, cancel);
|
||||||
@@ -97,6 +116,20 @@ public class CreateEnvelopeCommandHandler : IRequestHandler<CreateEnvelopeComman
|
|||||||
envelope.Title = title;
|
envelope.Title = title;
|
||||||
envelope.Message = message;
|
envelope.Message = message;
|
||||||
envelope.TfaEnabled = request.TFAEnabled;
|
envelope.TfaEnabled = request.TFAEnabled;
|
||||||
|
envelope.EnvelopeTypeId = settings.EnvelopeTypeId;
|
||||||
|
envelope.Language = settings.Language;
|
||||||
|
envelope.UseAccessCode = settings.UseAccessCode;
|
||||||
|
envelope.CertificationType = settings.CertificationType;
|
||||||
|
envelope.FinalEmailToCreator = settings.FinalEmailToCreator;
|
||||||
|
envelope.FinalEmailToReceivers = settings.FinalEmailToReceivers;
|
||||||
|
envelope.SendReminderEmails = settings.SendReminderEmails;
|
||||||
|
envelope.FirstReminderDays = settings.FirstReminderDays;
|
||||||
|
envelope.ReminderIntervalDays = settings.ReminderIntervalDays;
|
||||||
|
envelope.ExpiresWhenDays = settings.ExpiresWhenDays;
|
||||||
|
envelope.ExpiresWarningWhenDays = settings.ExpiresWarningWhenDays;
|
||||||
|
envelope.ContractType = settings.ContractType;
|
||||||
|
envelope.ExpiresWhen = settings.ExpiresWhen;
|
||||||
|
envelope.ExpiresWarningWhen = settings.ExpiresWarningWhen;
|
||||||
envelope.Status = EnvelopeStatus.EnvelopeSaved;
|
envelope.Status = EnvelopeStatus.EnvelopeSaved;
|
||||||
envelope.ChangedWhen = now;
|
envelope.ChangedWhen = now;
|
||||||
}
|
}
|
||||||
@@ -114,12 +147,69 @@ public class CreateEnvelopeCommandHandler : IRequestHandler<CreateEnvelopeComman
|
|||||||
AddedWhen = now,
|
AddedWhen = now,
|
||||||
ChangedWhen = now,
|
ChangedWhen = now,
|
||||||
Comment = string.Empty,
|
Comment = string.Empty,
|
||||||
Language = "de-DE",
|
EnvelopeTypeId = settings.EnvelopeTypeId,
|
||||||
|
Language = settings.Language,
|
||||||
TfaEnabled = request.TFAEnabled,
|
TfaEnabled = request.TFAEnabled,
|
||||||
UseAccessCode = false,
|
UseAccessCode = settings.UseAccessCode,
|
||||||
SendReminderEmails = false
|
CertificationType = settings.CertificationType,
|
||||||
|
FinalEmailToCreator = settings.FinalEmailToCreator,
|
||||||
|
FinalEmailToReceivers = settings.FinalEmailToReceivers,
|
||||||
|
SendReminderEmails = settings.SendReminderEmails,
|
||||||
|
FirstReminderDays = settings.FirstReminderDays,
|
||||||
|
ReminderIntervalDays = settings.ReminderIntervalDays,
|
||||||
|
ExpiresWhenDays = settings.ExpiresWhenDays,
|
||||||
|
ExpiresWarningWhenDays = settings.ExpiresWarningWhenDays,
|
||||||
|
ExpiresWhen = settings.ExpiresWhen,
|
||||||
|
ExpiresWarningWhen = settings.ExpiresWarningWhen,
|
||||||
|
ContractType = settings.ContractType
|
||||||
}, cancel);
|
}, cancel);
|
||||||
|
|
||||||
|
if (request.UseSQLExecutor)
|
||||||
|
{
|
||||||
|
await Repository.UpdateAsync(e =>
|
||||||
|
{
|
||||||
|
e.Title = title;
|
||||||
|
e.Message = message;
|
||||||
|
e.TfaEnabled = request.TFAEnabled;
|
||||||
|
e.EnvelopeTypeId = settings.EnvelopeTypeId;
|
||||||
|
e.Language = settings.Language;
|
||||||
|
e.UseAccessCode = settings.UseAccessCode;
|
||||||
|
e.CertificationType = settings.CertificationType;
|
||||||
|
e.FinalEmailToCreator = settings.FinalEmailToCreator;
|
||||||
|
e.FinalEmailToReceivers = settings.FinalEmailToReceivers;
|
||||||
|
e.SendReminderEmails = settings.SendReminderEmails;
|
||||||
|
e.FirstReminderDays = settings.FirstReminderDays;
|
||||||
|
e.ReminderIntervalDays = settings.ReminderIntervalDays;
|
||||||
|
e.ExpiresWhenDays = settings.ExpiresWhenDays;
|
||||||
|
e.ExpiresWarningWhenDays = settings.ExpiresWarningWhenDays;
|
||||||
|
e.ExpiresWhen = settings.ExpiresWhen;
|
||||||
|
e.ExpiresWarningWhen = settings.ExpiresWarningWhen;
|
||||||
|
e.ContractType = settings.ContractType;
|
||||||
|
e.Status = EnvelopeStatus.EnvelopeSaved;
|
||||||
|
e.ChangedWhen = now;
|
||||||
|
}, e => e.Id == envelope.Id, cancel);
|
||||||
|
|
||||||
|
envelope.Title = title;
|
||||||
|
envelope.Message = message;
|
||||||
|
envelope.TfaEnabled = request.TFAEnabled;
|
||||||
|
envelope.EnvelopeTypeId = settings.EnvelopeTypeId;
|
||||||
|
envelope.Language = settings.Language;
|
||||||
|
envelope.UseAccessCode = settings.UseAccessCode;
|
||||||
|
envelope.CertificationType = settings.CertificationType;
|
||||||
|
envelope.FinalEmailToCreator = settings.FinalEmailToCreator;
|
||||||
|
envelope.FinalEmailToReceivers = settings.FinalEmailToReceivers;
|
||||||
|
envelope.SendReminderEmails = settings.SendReminderEmails;
|
||||||
|
envelope.FirstReminderDays = settings.FirstReminderDays;
|
||||||
|
envelope.ReminderIntervalDays = settings.ReminderIntervalDays;
|
||||||
|
envelope.ExpiresWhenDays = settings.ExpiresWhenDays;
|
||||||
|
envelope.ExpiresWarningWhenDays = settings.ExpiresWarningWhenDays;
|
||||||
|
envelope.ExpiresWhen = settings.ExpiresWhen;
|
||||||
|
envelope.ExpiresWarningWhen = settings.ExpiresWarningWhen;
|
||||||
|
envelope.ContractType = settings.ContractType;
|
||||||
|
envelope.Status = EnvelopeStatus.EnvelopeSaved;
|
||||||
|
envelope.ChangedWhen = now;
|
||||||
|
}
|
||||||
|
|
||||||
await Mediator.Send(new CreateHistoryCommand
|
await Mediator.Send(new CreateHistoryCommand
|
||||||
{
|
{
|
||||||
EnvelopeId = envelope.Id,
|
EnvelopeId = envelope.Id,
|
||||||
@@ -143,6 +233,105 @@ public class CreateEnvelopeCommandHandler : IRequestHandler<CreateEnvelopeComman
|
|||||||
return _mapper.Map<EnvelopeDto>(envelope);
|
return _mapper.Map<EnvelopeDto>(envelope);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private async Task<EnvelopeSettings> ResolveEnvelopeSettingsAsync(CreateEnvelopeCommand request, Envelope? existing, CancellationToken cancel)
|
||||||
|
{
|
||||||
|
EnvelopeType? selectedType = null;
|
||||||
|
if (request.EnvelopeTypeId is int envelopeTypeId && envelopeTypeId > 0)
|
||||||
|
{
|
||||||
|
selectedType = await EnvelopeTypeRepository.Query
|
||||||
|
.FirstOrDefaultAsync(t => t.Id == envelopeTypeId, cancel)
|
||||||
|
?? throw new BadRequestException($"Envelope type {envelopeTypeId} not found.");
|
||||||
|
}
|
||||||
|
|
||||||
|
var expiresWhenDays = Math.Max(0,
|
||||||
|
request.ExpiresWhenDays
|
||||||
|
?? selectedType?.ExpiresWhenDays
|
||||||
|
?? existing?.ExpiresWhenDays
|
||||||
|
?? 0);
|
||||||
|
|
||||||
|
var expiresWarningWhenDays = Math.Max(0,
|
||||||
|
request.ExpiresWarningWhenDays
|
||||||
|
?? selectedType?.ExpiresWarningWhenDays
|
||||||
|
?? existing?.ExpiresWarningWhenDays
|
||||||
|
?? 0);
|
||||||
|
|
||||||
|
var useAccessCode = request.UseAccessCode
|
||||||
|
?? selectedType?.UseAccessCode
|
||||||
|
?? existing?.UseAccessCode
|
||||||
|
?? false;
|
||||||
|
|
||||||
|
if (request.TFAEnabled)
|
||||||
|
useAccessCode = true;
|
||||||
|
|
||||||
|
return new EnvelopeSettings(
|
||||||
|
EnvelopeTypeId: request.EnvelopeTypeId ?? existing?.EnvelopeTypeId,
|
||||||
|
Language: ResolveLanguage(request.Language, selectedType?.Language, existing?.Language),
|
||||||
|
UseAccessCode: useAccessCode,
|
||||||
|
CertificationType: request.CertificationType
|
||||||
|
?? (int?)selectedType?.CertificationType
|
||||||
|
?? existing?.CertificationType
|
||||||
|
?? (int)CertificationType.AdvancedElectronicSignature,
|
||||||
|
FinalEmailToCreator: request.FinalEmailToCreator
|
||||||
|
?? (int?)selectedType?.FinalEmailToCreator
|
||||||
|
?? existing?.FinalEmailToCreator
|
||||||
|
?? (int)FinalEmailType.No,
|
||||||
|
FinalEmailToReceivers: request.FinalEmailToReceivers
|
||||||
|
?? (int?)selectedType?.FinalEmailToReceivers
|
||||||
|
?? existing?.FinalEmailToReceivers
|
||||||
|
?? (int)FinalEmailType.No,
|
||||||
|
SendReminderEmails: request.SendReminderEmails
|
||||||
|
?? selectedType?.SendReminderEmails
|
||||||
|
?? existing?.SendReminderEmails
|
||||||
|
?? false,
|
||||||
|
FirstReminderDays: Math.Max(0,
|
||||||
|
request.FirstReminderDays
|
||||||
|
?? selectedType?.FirstReminderDays
|
||||||
|
?? existing?.FirstReminderDays
|
||||||
|
?? 0),
|
||||||
|
ReminderIntervalDays: Math.Max(0,
|
||||||
|
request.ReminderIntervalDays
|
||||||
|
?? selectedType?.ReminderIntervalDays
|
||||||
|
?? existing?.ReminderIntervalDays
|
||||||
|
?? 0),
|
||||||
|
ExpiresWhenDays: expiresWhenDays,
|
||||||
|
ExpiresWarningWhenDays: expiresWarningWhenDays,
|
||||||
|
ExpiresWhen: expiresWhenDays > 0 ? DateTime.Now.AddDays(expiresWhenDays) : null,
|
||||||
|
ExpiresWarningWhen: expiresWarningWhenDays > 0 ? DateTime.Now.AddDays(expiresWarningWhenDays) : null,
|
||||||
|
ContractType: existing?.ContractType
|
||||||
|
?? selectedType?.ContractType
|
||||||
|
?? (int)ContractType.Contract);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static string ResolveLanguage(string? preferred, string? fallback1, string? fallback2)
|
||||||
|
{
|
||||||
|
if (!string.IsNullOrWhiteSpace(preferred))
|
||||||
|
return preferred.Trim();
|
||||||
|
|
||||||
|
if (!string.IsNullOrWhiteSpace(fallback1))
|
||||||
|
return fallback1.Trim();
|
||||||
|
|
||||||
|
if (!string.IsNullOrWhiteSpace(fallback2))
|
||||||
|
return fallback2.Trim();
|
||||||
|
|
||||||
|
return "de-DE";
|
||||||
|
}
|
||||||
|
|
||||||
|
private sealed record EnvelopeSettings(
|
||||||
|
int? EnvelopeTypeId,
|
||||||
|
string Language,
|
||||||
|
bool UseAccessCode,
|
||||||
|
int CertificationType,
|
||||||
|
int FinalEmailToCreator,
|
||||||
|
int FinalEmailToReceivers,
|
||||||
|
bool SendReminderEmails,
|
||||||
|
int FirstReminderDays,
|
||||||
|
int ReminderIntervalDays,
|
||||||
|
int ExpiresWhenDays,
|
||||||
|
int ExpiresWarningWhenDays,
|
||||||
|
DateTime? ExpiresWhen,
|
||||||
|
DateTime? ExpiresWarningWhen,
|
||||||
|
int ContractType);
|
||||||
|
|
||||||
private async Task<string> ResolveSenderReferenceAsync(Envelope envelope, CancellationToken cancel)
|
private async Task<string> ResolveSenderReferenceAsync(Envelope envelope, CancellationToken cancel)
|
||||||
{
|
{
|
||||||
if (!string.IsNullOrWhiteSpace(envelope.User?.Email))
|
if (!string.IsNullOrWhiteSpace(envelope.User?.Email))
|
||||||
|
|||||||
@@ -22,41 +22,41 @@ namespace EnvelopeGenerator.Domain.Entities
|
|||||||
public string Language { get; set; }
|
public string Language { get; set; }
|
||||||
|
|
||||||
[Column("EXPIRES_DAYS")]
|
[Column("EXPIRES_DAYS")]
|
||||||
public int ExpiresDays { get; set; }
|
public int? ExpiresDays { get; set; }
|
||||||
|
|
||||||
[Column("CERTIFICATION_TYPE")]
|
[Column("CERTIFICATION_TYPE")]
|
||||||
public Constants.CertificationType CertificationType { get; set; } = Constants.CertificationType.AdvancedElectronicSignature;
|
public Constants.CertificationType? CertificationType { get; set; }
|
||||||
|
|
||||||
[Column("USE_ACCESS_CODE")]
|
[Column("USE_ACCESS_CODE")]
|
||||||
public bool UseAccessCode { get; set; }
|
public bool? UseAccessCode { get; set; }
|
||||||
|
|
||||||
[Column("FINAL_EMAIL_TO_CREATOR")]
|
[Column("FINAL_EMAIL_TO_CREATOR")]
|
||||||
public Constants.FinalEmailType FinalEmailToCreator { get; set; }
|
public Constants.FinalEmailType? FinalEmailToCreator { get; set; }
|
||||||
|
|
||||||
[Column("FINAL_EMAIL_TO_RECEIVERS")]
|
[Column("FINAL_EMAIL_TO_RECEIVERS")]
|
||||||
public Constants.FinalEmailType FinalEmailToReceivers { get; set; }
|
public Constants.FinalEmailType? FinalEmailToReceivers { get; set; }
|
||||||
|
|
||||||
[Required]
|
[Required]
|
||||||
[Column("ADDED_WHEN", TypeName = "datetime")]
|
[Column("ADDED_WHEN", TypeName = "datetime")]
|
||||||
public DateTime AddedWhen { get; set; }
|
public DateTime AddedWhen { get; set; }
|
||||||
|
|
||||||
[Column("CHANGED_WHEN", TypeName = "datetime")]
|
[Column("CHANGED_WHEN", TypeName = "datetime")]
|
||||||
public DateTime ChangedWhen { get; set; }
|
public DateTime? ChangedWhen { get; set; }
|
||||||
|
|
||||||
[Column("EXPIRES_WARNING_DAYS")]
|
[Column("EXPIRES_WARNING_DAYS")]
|
||||||
public int ExpiresWarningDays { get; set; }
|
public int? ExpiresWarningDays { get; set; }
|
||||||
|
|
||||||
[Column("SEND_REMINDER_EMAILS")]
|
[Column("SEND_REMINDER_EMAILS")]
|
||||||
public bool SendReminderEmails { get; set; } = false;
|
public bool? SendReminderEmails { get; set; }
|
||||||
|
|
||||||
[Column("FIRST_REMINDER_DAYS")]
|
[Column("FIRST_REMINDER_DAYS")]
|
||||||
public int FirstReminderDays { get; set; } = 0;
|
public int? FirstReminderDays { get; set; }
|
||||||
|
|
||||||
[Column("REMINDER_INTERVAL_DAYS")]
|
[Column("REMINDER_INTERVAL_DAYS")]
|
||||||
public int ReminderIntervalDays { get; set; } = 0;
|
public int? ReminderIntervalDays { get; set; }
|
||||||
|
|
||||||
[Column("CONTRACT_TYPE")]
|
[Column("CONTRACT_TYPE")]
|
||||||
public int ContractType { get; set; }
|
public int? ContractType { get; set; }
|
||||||
|
|
||||||
[NotMapped]
|
[NotMapped]
|
||||||
public bool TFA_Enabled { get; set; } = false;
|
public bool TFA_Enabled { get; set; } = false;
|
||||||
@@ -70,14 +70,14 @@ namespace EnvelopeGenerator.Domain.Entities
|
|||||||
[NotMapped]
|
[NotMapped]
|
||||||
public int ExpiresWhenDays
|
public int ExpiresWhenDays
|
||||||
{
|
{
|
||||||
get => ExpiresDays;
|
get => ExpiresDays ?? 0;
|
||||||
set => ExpiresDays = value;
|
set => ExpiresDays = value;
|
||||||
}
|
}
|
||||||
|
|
||||||
[NotMapped]
|
[NotMapped]
|
||||||
public int ExpiresWarningWhenDays
|
public int ExpiresWarningWhenDays
|
||||||
{
|
{
|
||||||
get => ExpiresWarningDays;
|
get => ExpiresWarningDays ?? 0;
|
||||||
set => ExpiresWarningDays = value;
|
set => ExpiresWarningDays = value;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
18
EnvelopeGenerator.Infrastructure.Doc/DependencyInjection.cs
Normal file
18
EnvelopeGenerator.Infrastructure.Doc/DependencyInjection.cs
Normal file
@@ -0,0 +1,18 @@
|
|||||||
|
using EnvelopeGenerator.Application.Common.Interfaces.Doc;
|
||||||
|
using Microsoft.Extensions.DependencyInjection;
|
||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Linq;
|
||||||
|
using System.Text;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
|
||||||
|
namespace EnvelopeGenerator.Infrastructure.Doc;
|
||||||
|
|
||||||
|
public static class DependencyInjection
|
||||||
|
{
|
||||||
|
public static IServiceCollection AddDocInfrastructure(this IServiceCollection services)
|
||||||
|
{
|
||||||
|
services.AddScoped<IPdfPageImageRenderer, PdfPageImageRenderer>();
|
||||||
|
return services;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -47,9 +47,7 @@
|
|||||||
{
|
{
|
||||||
<div class="sender-image-editor__empty">@L("SenderEditor_NoPagesToDisplay", "Keine Seiten zur Anzeige verfügbar.")</div>
|
<div class="sender-image-editor__empty">@L("SenderEditor_NoPagesToDisplay", "Keine Seiten zur Anzeige verfügbar.")</div>
|
||||||
}
|
}
|
||||||
else
|
else if (Pages.Count == 0 && !string.IsNullOrWhiteSpace(PdfDataUrl))
|
||||||
{
|
|
||||||
@if (Pages.Count == 0 && !string.IsNullOrWhiteSpace(PdfDataUrl))
|
|
||||||
{
|
{
|
||||||
<div class="sender-image-editor__native-preview" style="@GetZoomStyle()">
|
<div class="sender-image-editor__native-preview" style="@GetZoomStyle()">
|
||||||
<iframe class="sender-image-editor__native-frame"
|
<iframe class="sender-image-editor__native-frame"
|
||||||
@@ -57,7 +55,8 @@
|
|||||||
title="PDF Preview"></iframe>
|
title="PDF Preview"></iframe>
|
||||||
</div>
|
</div>
|
||||||
}
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
<div class="sender-image-editor__pages" style="@GetZoomStyle()">
|
<div class="sender-image-editor__pages" style="@GetZoomStyle()">
|
||||||
@for (var pageIndex = 0; pageIndex < PageCount; pageIndex++)
|
@for (var pageIndex = 0; pageIndex < PageCount; pageIndex++)
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -80,7 +80,7 @@
|
|||||||
_selected = option;
|
_selected = option;
|
||||||
_isOpen = false;
|
_isOpen = false;
|
||||||
await LocalizationService.SetLanguageAsync(option.Code);
|
await LocalizationService.SetLanguageAsync(option.Code);
|
||||||
Navigation.NavigateTo(Navigation.Uri, forceLoad: true);
|
Navigation.NavigateTo(Navigation.Uri, forceLoad: false);
|
||||||
}
|
}
|
||||||
|
|
||||||
private sealed record LanguageOption(string Code, string FlagClass, string ShortName);
|
private sealed record LanguageOption(string Code, string FlagClass, string ShortName);
|
||||||
|
|||||||
@@ -1,24 +1,22 @@
|
|||||||
@page "/sender"
|
@page "/sender"
|
||||||
@rendermode InteractiveServer
|
@rendermode InteractiveWebAssembly
|
||||||
@using System.Text.Json
|
@using System.Text.Json
|
||||||
@using EnvelopeGenerator.Domain.Constants
|
@using EnvelopeGenerator.Domain.Constants
|
||||||
@using EnvelopeGenerator.Server.Client.Models
|
@using EnvelopeGenerator.Server.Client.Models
|
||||||
@using DevExpress.Blazor
|
@using DevExpress.Blazor
|
||||||
@using DevExpress.Blazor.PdfViewer
|
|
||||||
@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.Resources
|
@using EnvelopeGenerator.Application.Resources
|
||||||
@using EnvelopeGenerator.Domain.Resources
|
@using EnvelopeGenerator.Domain.Resources
|
||||||
@using EnvelopeGenerator.Server.Services
|
|
||||||
@using Microsoft.Extensions.Localization
|
@using Microsoft.Extensions.Localization
|
||||||
@inject EnvelopeGenerator.Server.Client.Services.EnvelopeService EnvelopeService
|
@inject EnvelopeGenerator.Server.Client.Services.EnvelopeService EnvelopeService
|
||||||
|
@inject EnvelopeGenerator.Server.Client.Services.DocumentService DocumentService
|
||||||
|
@inject EnvelopeGenerator.Server.Client.Services.PdfRenderService PdfRenderService
|
||||||
@inject EnvelopeGenerator.Server.Client.Services.AuthService AuthService
|
@inject EnvelopeGenerator.Server.Client.Services.AuthService AuthService
|
||||||
@inject EnvelopeGenerator.Server.Client.Services.ThemeService ThemeService
|
@inject EnvelopeGenerator.Server.Client.Services.ThemeService ThemeService
|
||||||
@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 EnvelopeReceiverPageDataService ReceiverPageDataService
|
|
||||||
@inject IEnvelopeAuthService EnvelopeAuthService
|
|
||||||
@inject ILogger<EnvelopeSenderPage> Logger
|
@inject ILogger<EnvelopeSenderPage> Logger
|
||||||
@inject NavigationManager Navigation
|
@inject NavigationManager Navigation
|
||||||
@inject IJSRuntime JSRuntime
|
@inject IJSRuntime JSRuntime
|
||||||
@@ -262,7 +260,7 @@
|
|||||||
var envelope = cellContext.DataItem as EnvelopeDto;
|
var envelope = cellContext.DataItem as EnvelopeDto;
|
||||||
if (envelope != null)
|
if (envelope != null)
|
||||||
{
|
{
|
||||||
var statusInfo = GetStatusInfo(envelope.Status);
|
var statusInfo = GetStatusInfo(GetEffectiveEnvelopeStatus(envelope));
|
||||||
<div class="status-badge status-badge--@statusInfo.CssClass">
|
<div class="status-badge status-badge--@statusInfo.CssClass">
|
||||||
<span class="status-dot status-dot--@statusInfo.DotColor"></span>
|
<span class="status-dot status-dot--@statusInfo.DotColor"></span>
|
||||||
@statusInfo.Label
|
@statusInfo.Label
|
||||||
@@ -314,12 +312,20 @@
|
|||||||
{
|
{
|
||||||
<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 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);
|
||||||
}
|
}
|
||||||
<span class="receiver-badge receiver-badge--@(isSigned ? "signed" : "unsigned")" style="min-width: 100px;">
|
<span class="receiver-badge receiver-badge--@(isRejected ? "unsigned" : (isSigned ? "signed" : "unsigned"))" style="min-width: 100px;">
|
||||||
@if (isSigned)
|
@if (isRejected)
|
||||||
|
{
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" width="12" height="12" fill="currentColor" viewBox="0 0 16 16">
|
||||||
|
<path d="M16 8A8 8 0 1 1 0 8a8 8 0 0 1 16 0zM5.354 4.646a.5.5 0 1 0-.708.708L7.293 8l-2.647 2.646a.5.5 0 0 0 .708.708L8 8.707l2.646 2.647a.5.5 0 0 0 .708-.708L8.707 8l2.647-2.646a.5.5 0 0 0-.708-.708L8 7.293 5.354 4.646z" />
|
||||||
|
</svg>
|
||||||
|
<span>@GetStatusLabel(EnvelopeStatus.DocumentRejected)</span>
|
||||||
|
}
|
||||||
|
else if (isSigned)
|
||||||
{
|
{
|
||||||
<svg xmlns="http://www.w3.org/2000/svg" width="12" height="12" fill="currentColor" viewBox="0 0 16 16">
|
<svg xmlns="http://www.w3.org/2000/svg" width="12" height="12" fill="currentColor" viewBox="0 0 16 16">
|
||||||
<path d="M13.854 3.646a.5.5 0 0 1 0 .708l-7 7a.5.5 0 0 1-.708 0l-3.5-3.5a.5.5 0 1 1 .708-.708L6.5 10.293l6.646-6.647a.5.5 0 0 1 .708 0z" />
|
<path d="M13.854 3.646a.5.5 0 0 1 0 .708l-7 7a.5.5 0 0 1-.708 0l-3.5-3.5a.5.5 0 1 1 .708-.708L6.5 10.293l6.646-6.647a.5.5 0 0 1 .708 0z" />
|
||||||
@@ -459,10 +465,15 @@
|
|||||||
else if (_previewPdfBytes is not null && _previewViewerMounted)
|
else if (_previewPdfBytes is not null && _previewViewerMounted)
|
||||||
{
|
{
|
||||||
<div class="sender-preview-viewer-wrap">
|
<div class="sender-preview-viewer-wrap">
|
||||||
<DxPdfViewer DocumentContent="@_previewPdfBytes"
|
<SenderPdfImageEditor IsPlacementMode="false"
|
||||||
ZoomLevel="-2"
|
PageCount="@_previewPageCount"
|
||||||
IsSinglePagePreview="true"
|
Pages="@_previewPagePreviews"
|
||||||
CssClass="sender-preview-pdf-viewer" />
|
PdfDataUrl="@PreviewPdfDataUrl"
|
||||||
|
PdfFileName="@_previewFileName"
|
||||||
|
Fields="@_previewFieldOverlays"
|
||||||
|
EditorClick="OnPreviewEditorClickAsync"
|
||||||
|
RemoveField="OnPreviewRemoveFieldAsync"
|
||||||
|
MoveField="OnPreviewMoveFieldAsync" />
|
||||||
</div>
|
</div>
|
||||||
}
|
}
|
||||||
</BodyContentTemplate>
|
</BodyContentTemplate>
|
||||||
@@ -536,7 +547,15 @@
|
|||||||
private bool _previewLoading = false;
|
private bool _previewLoading = false;
|
||||||
private bool _previewViewerMounted = false;
|
private bool _previewViewerMounted = false;
|
||||||
private byte[]? _previewPdfBytes = null;
|
private byte[]? _previewPdfBytes = null;
|
||||||
|
private int _previewPageCount = 0;
|
||||||
|
private List<PdfPageImageDto> _previewPagePreviews = [];
|
||||||
|
private string _previewFileName = "document.pdf";
|
||||||
private string? _previewErrorMessage = null;
|
private string? _previewErrorMessage = null;
|
||||||
|
private readonly IReadOnlyList<SenderPdfFieldOverlay> _previewFieldOverlays = [];
|
||||||
|
|
||||||
|
private string? PreviewPdfDataUrl => _previewPdfBytes is { Length: > 0 }
|
||||||
|
? $"data:application/pdf;base64,{Convert.ToBase64String(_previewPdfBytes)}"
|
||||||
|
: null;
|
||||||
|
|
||||||
// Phase 6e (step 1): UI state only. Export logic is added in step 2.
|
// Phase 6e (step 1): UI state only. Export logic is added in step 2.
|
||||||
private string _selectedExportFormat = "xlsx";
|
private string _selectedExportFormat = "xlsx";
|
||||||
@@ -590,10 +609,11 @@
|
|||||||
{
|
{
|
||||||
_allEnvelopes = await EnvelopeService.GetAsync() ?? [];
|
_allEnvelopes = await EnvelopeService.GetAsync() ?? [];
|
||||||
|
|
||||||
// Split into active and completed based on status
|
// Split into active and completed based on effective status
|
||||||
|
// (history-aware: rejected envelopes should not stay in active tab).
|
||||||
var envelopes = _allEnvelopes.ToList();
|
var envelopes = _allEnvelopes.ToList();
|
||||||
_activeEnvelopes = envelopes.Where(e => ((EnvelopeStatus)e.Status).IsActive()).ToList();
|
_activeEnvelopes = envelopes.Where(e => GetEffectiveEnvelopeStatus(e).IsActive()).ToList();
|
||||||
_completedEnvelopes = envelopes.Where(e => ((EnvelopeStatus)e.Status).IsCompleted()).ToList();
|
_completedEnvelopes = envelopes.Where(e => GetEffectiveEnvelopeStatus(e).IsCompleted()).ToList();
|
||||||
}
|
}
|
||||||
catch (Exception ex)
|
catch (Exception ex)
|
||||||
{
|
{
|
||||||
@@ -740,18 +760,18 @@
|
|||||||
await InvokeAsync(StateHasChanged);
|
await InvokeAsync(StateHasChanged);
|
||||||
UserPreferencesService.InvalidateCache();
|
UserPreferencesService.InvalidateCache();
|
||||||
await AuthService.LogoutSenderAsync();
|
await AuthService.LogoutSenderAsync();
|
||||||
Navigation.NavigateTo("/sender/login", forceLoad: true);
|
Navigation.NavigateTo("/sender/login", forceLoad: false);
|
||||||
}
|
}
|
||||||
|
|
||||||
bool IsEnvelopeSent(EnvelopeDto envelope)
|
bool IsEnvelopeSent(EnvelopeDto envelope)
|
||||||
{
|
{
|
||||||
var status = (EnvelopeStatus)envelope.Status;
|
var status = GetEffectiveEnvelopeStatus(envelope);
|
||||||
return status >= EnvelopeStatus.EnvelopeQueued;
|
return status >= EnvelopeStatus.EnvelopeQueued;
|
||||||
}
|
}
|
||||||
|
|
||||||
bool IsEnvelopeEditable(EnvelopeDto envelope)
|
bool IsEnvelopeEditable(EnvelopeDto envelope)
|
||||||
{
|
{
|
||||||
var status = (EnvelopeStatus)envelope.Status;
|
var status = GetEffectiveEnvelopeStatus(envelope);
|
||||||
return status is EnvelopeStatus.EnvelopeCreated or EnvelopeStatus.EnvelopeSaved;
|
return status is EnvelopeStatus.EnvelopeCreated or EnvelopeStatus.EnvelopeSaved;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -800,7 +820,7 @@
|
|||||||
IReadOnlyList<StatusFilterItem> GetStatusFilterItems(IEnumerable<EnvelopeDto>? source)
|
IReadOnlyList<StatusFilterItem> GetStatusFilterItems(IEnumerable<EnvelopeDto>? source)
|
||||||
{
|
{
|
||||||
return (source ?? Enumerable.Empty<EnvelopeDto>())
|
return (source ?? Enumerable.Empty<EnvelopeDto>())
|
||||||
.Select(x => x.Status)
|
.Select(GetEffectiveEnvelopeStatus)
|
||||||
.Distinct()
|
.Distinct()
|
||||||
.OrderBy(x => x)
|
.OrderBy(x => x)
|
||||||
.Select(x => new StatusFilterItem(x, GetStatusLabel(x)))
|
.Select(x => new StatusFilterItem(x, GetStatusLabel(x)))
|
||||||
@@ -818,7 +838,7 @@
|
|||||||
|
|
||||||
bool IsReceiverSigned(EnvelopeDto envelope, EnvelopeGenerator.Application.Common.Dto.EnvelopeReceiver.EnvelopeReceiverDto receiver)
|
bool IsReceiverSigned(EnvelopeDto envelope, EnvelopeGenerator.Application.Common.Dto.EnvelopeReceiver.EnvelopeReceiverDto receiver)
|
||||||
{
|
{
|
||||||
var status = (EnvelopeStatus)envelope.Status;
|
var status = GetEffectiveEnvelopeStatus(envelope);
|
||||||
if (status is EnvelopeStatus.EnvelopeCompletelySigned or EnvelopeStatus.MessageCompletionSent)
|
if (status is EnvelopeStatus.EnvelopeCompletelySigned or EnvelopeStatus.MessageCompletionSent)
|
||||||
return true;
|
return true;
|
||||||
|
|
||||||
@@ -841,6 +861,43 @@
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
bool IsReceiverRejected(EnvelopeDto envelope, EnvelopeGenerator.Application.Common.Dto.EnvelopeReceiver.EnvelopeReceiverDto receiver)
|
||||||
|
{
|
||||||
|
var status = GetEffectiveEnvelopeStatus(envelope);
|
||||||
|
var histories = envelope.Histories?.Where(h =>
|
||||||
|
h.Status is EnvelopeStatus.DocumentRejected or EnvelopeStatus.EnvelopeRejected).ToList() ?? [];
|
||||||
|
|
||||||
|
if (histories.Count == 0)
|
||||||
|
return false;
|
||||||
|
|
||||||
|
if (histories.Any(h => h.Receiver?.Id == receiver.ReceiverId))
|
||||||
|
return true;
|
||||||
|
|
||||||
|
var receiverEmail = receiver.Receiver?.EmailAddress;
|
||||||
|
if (!string.IsNullOrWhiteSpace(receiverEmail) &&
|
||||||
|
histories.Any(h => string.Equals(h.UserReference, receiverEmail, StringComparison.OrdinalIgnoreCase)))
|
||||||
|
return true;
|
||||||
|
|
||||||
|
if (!string.IsNullOrWhiteSpace(receiver.Name) &&
|
||||||
|
histories.Any(h => string.Equals(h.UserReference, receiver.Name, StringComparison.OrdinalIgnoreCase)))
|
||||||
|
return true;
|
||||||
|
|
||||||
|
return status == EnvelopeStatus.EnvelopeRejected && (envelope.EnvelopeReceivers?.Count() ?? 0) == 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
EnvelopeStatus GetEffectiveEnvelopeStatus(EnvelopeDto envelope)
|
||||||
|
{
|
||||||
|
var rawStatus = envelope.Status;
|
||||||
|
|
||||||
|
if (rawStatus is EnvelopeStatus.EnvelopeRejected or EnvelopeStatus.EnvelopeWithdrawn)
|
||||||
|
return rawStatus;
|
||||||
|
|
||||||
|
var hasRejectedHistory = (envelope.Histories ?? Enumerable.Empty<HistoryDto>())
|
||||||
|
.Any(h => h.Status is EnvelopeStatus.DocumentRejected or EnvelopeStatus.EnvelopeRejected);
|
||||||
|
|
||||||
|
return hasRejectedHistory ? EnvelopeStatus.EnvelopeRejected : rawStatus;
|
||||||
|
}
|
||||||
|
|
||||||
DateTime GetHistoryDate(HistoryDto history)
|
DateTime GetHistoryDate(HistoryDto history)
|
||||||
{
|
{
|
||||||
return history.ActionDate ?? history.AddedWhen;
|
return history.ActionDate ?? history.AddedWhen;
|
||||||
@@ -916,6 +973,9 @@
|
|||||||
// Unmount the viewer first — let the previous instance fully dispose
|
// Unmount the viewer first — let the previous instance fully dispose
|
||||||
_previewViewerMounted = false;
|
_previewViewerMounted = false;
|
||||||
_previewPdfBytes = null;
|
_previewPdfBytes = null;
|
||||||
|
_previewPageCount = 0;
|
||||||
|
_previewPagePreviews = [];
|
||||||
|
_previewFileName = $"envelope-{envelope.Id}.pdf";
|
||||||
_previewErrorMessage = null;
|
_previewErrorMessage = null;
|
||||||
_previewLoading = true;
|
_previewLoading = true;
|
||||||
_previewVisible = true;
|
_previewVisible = true;
|
||||||
@@ -923,19 +983,35 @@
|
|||||||
|
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
var user = EnvelopeAuthService.GetCurrentUser();
|
_previewPdfBytes = await DocumentService.GetSenderDocumentBytesByEnvelopeIdAsync(envelope.Id);
|
||||||
if (user?.Identity is null || !user.Identity.IsAuthenticated)
|
if (_previewPdfBytes is null || _previewPdfBytes.Length == 0)
|
||||||
{
|
{
|
||||||
_previewErrorMessage = L("NotAuthenticatedPleaseLoginAgain", "Nicht authentifiziert. Bitte melden Sie sich erneut an.");
|
_previewErrorMessage = L("DocumentCouldNotBeLoaded", "Dokument konnte nicht geladen werden");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
_previewPdfBytes = await ReceiverPageDataService.GetDocumentOfSenderAsync(user, envelope.Id);
|
var previewPdfBytes = _previewPdfBytes;
|
||||||
|
|
||||||
|
_previewPagePreviews = [];
|
||||||
|
try
|
||||||
|
{
|
||||||
|
_previewPagePreviews = (await PdfRenderService.RenderPagesAsync(previewPdfBytes))
|
||||||
|
.OrderBy(p => p.PageNumber)
|
||||||
|
.ToList();
|
||||||
|
}
|
||||||
|
catch (Exception renderEx)
|
||||||
|
{
|
||||||
|
Logger.LogWarning(renderEx, "Preview: page image rendering failed for envelope {EnvelopeId}. Falling back to native PDF preview.", envelope.Id);
|
||||||
|
}
|
||||||
|
|
||||||
|
_previewPageCount = _previewPagePreviews.Count > 0
|
||||||
|
? _previewPagePreviews.Count
|
||||||
|
: EstimatePdfPageCount(previewPdfBytes) ?? 1;
|
||||||
|
|
||||||
// PDF ready — mount the viewer now
|
// PDF ready — mount the viewer now
|
||||||
_previewViewerMounted = true;
|
_previewViewerMounted = true;
|
||||||
}
|
}
|
||||||
catch (DigitalData.Core.Exceptions.NotFoundException)
|
catch (HttpRequestException ex) when ((int?)ex.StatusCode == 404)
|
||||||
{
|
{
|
||||||
_previewErrorMessage = string.Format(
|
_previewErrorMessage = string.Format(
|
||||||
L("Sender_PreviewDocumentNotFound", "Das Dokument für Umschlag #{0} wurde nicht gefunden oder Sie haben keinen Zugriff darauf."),
|
L("Sender_PreviewDocumentNotFound", "Das Dokument für Umschlag #{0} wurde nicht gefunden oder Sie haben keinen Zugriff darauf."),
|
||||||
@@ -965,12 +1041,60 @@
|
|||||||
{
|
{
|
||||||
await Task.Yield(); // wait one render cycle for the viewer to be removed from the DOM
|
await Task.Yield(); // wait one render cycle for the viewer to be removed from the DOM
|
||||||
_previewPdfBytes = null;
|
_previewPdfBytes = null;
|
||||||
|
_previewPageCount = 0;
|
||||||
|
_previewPagePreviews = [];
|
||||||
|
_previewFileName = "document.pdf";
|
||||||
_previewErrorMessage = null;
|
_previewErrorMessage = null;
|
||||||
_previewLoading = false;
|
_previewLoading = false;
|
||||||
await InvokeAsync(StateHasChanged);
|
await InvokeAsync(StateHasChanged);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Task OnPreviewEditorClickAsync(Microsoft.AspNetCore.Components.Web.MouseEventArgs _)
|
||||||
|
{
|
||||||
|
return Task.CompletedTask;
|
||||||
|
}
|
||||||
|
|
||||||
|
Task OnPreviewRemoveFieldAsync(int _)
|
||||||
|
{
|
||||||
|
return Task.CompletedTask;
|
||||||
|
}
|
||||||
|
|
||||||
|
Task OnPreviewMoveFieldAsync(SenderPdfFieldMoveRequest _)
|
||||||
|
{
|
||||||
|
return Task.CompletedTask;
|
||||||
|
}
|
||||||
|
|
||||||
|
static int? EstimatePdfPageCount(byte[]? documentBytes)
|
||||||
|
{
|
||||||
|
if (documentBytes is null || documentBytes.Length == 0)
|
||||||
|
return null;
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var text = System.Text.Encoding.ASCII.GetString(documentBytes);
|
||||||
|
const string marker = "/Type /Page";
|
||||||
|
var count = 0;
|
||||||
|
var index = 0;
|
||||||
|
|
||||||
|
while (true)
|
||||||
|
{
|
||||||
|
index = text.IndexOf(marker, index, StringComparison.Ordinal);
|
||||||
|
if (index < 0)
|
||||||
|
break;
|
||||||
|
|
||||||
|
count++;
|
||||||
|
index += marker.Length;
|
||||||
|
}
|
||||||
|
|
||||||
|
return count > 0 ? count : null;
|
||||||
|
}
|
||||||
|
catch
|
||||||
|
{
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// ── Grid Layout Persistence ───────────────────────────────────────────────
|
// ── Grid Layout Persistence ───────────────────────────────────────────────
|
||||||
// LayoutAutoLoading fires when the grid initializes — restore saved layout.
|
// LayoutAutoLoading fires when the grid initializes — restore saved layout.
|
||||||
// LayoutAutoSaving fires when the user changes column order/width/sort — persist it.
|
// LayoutAutoSaving fires when the user changes column order/width/sort — persist it.
|
||||||
@@ -983,13 +1107,18 @@
|
|||||||
// If cache is empty (LoadFromServerAsync failed), skip layout restore silently.
|
// If cache is empty (LoadFromServerAsync failed), skip layout restore silently.
|
||||||
var prefs = UserPreferencesService.GetCached();
|
var prefs = UserPreferencesService.GetCached();
|
||||||
if (prefs is not null && prefs.GridLayouts.TryGetValue(CurrentGridKey, out var json))
|
if (prefs is not null && prefs.GridLayouts.TryGetValue(CurrentGridKey, out var json))
|
||||||
e.Layout = JsonSerializer.Deserialize<GridPersistentLayout>(json);
|
{
|
||||||
|
var layout = JsonSerializer.Deserialize<GridPersistentLayout>(json);
|
||||||
|
SanitizeLayoutFilters(layout);
|
||||||
|
e.Layout = layout;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async Task OnGridLayoutAutoSaving(GridPersistentLayoutEventArgs e)
|
async Task OnGridLayoutAutoSaving(GridPersistentLayoutEventArgs e)
|
||||||
{
|
{
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
|
SanitizeLayoutFilters(e.Layout);
|
||||||
var json = JsonSerializer.Serialize(e.Layout);
|
var json = JsonSerializer.Serialize(e.Layout);
|
||||||
await UserPreferencesService.SaveGridLayoutAsync(CurrentGridKey, json);
|
await UserPreferencesService.SaveGridLayoutAsync(CurrentGridKey, json);
|
||||||
}
|
}
|
||||||
@@ -999,5 +1128,55 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Keep column/order personalization, but never persist/restore data filters.
|
||||||
|
// Tab counters are based on tab datasets; persisted grid filters can hide rows and
|
||||||
|
// make the grid appear empty or incomplete.
|
||||||
|
static void SanitizeLayoutFilters(GridPersistentLayout? layout)
|
||||||
|
{
|
||||||
|
if (layout is null)
|
||||||
|
return;
|
||||||
|
|
||||||
|
ClearWritableProperty(layout, "FilterCriteria");
|
||||||
|
ClearWritableProperty(layout, "SearchText");
|
||||||
|
|
||||||
|
if (layout.Columns is not System.Collections.IEnumerable columns)
|
||||||
|
return;
|
||||||
|
|
||||||
|
foreach (var column in columns)
|
||||||
|
{
|
||||||
|
if (column is null)
|
||||||
|
continue;
|
||||||
|
|
||||||
|
ClearWritableProperty(column, "FilterCriteria");
|
||||||
|
ClearWritableProperty(column, "FilterValues");
|
||||||
|
ClearWritableProperty(column, "FilterValue");
|
||||||
|
ClearWritableProperty(column, "SearchText");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
static void ClearWritableProperty(object target, string propertyName)
|
||||||
|
{
|
||||||
|
var prop = target.GetType()
|
||||||
|
.GetProperties(System.Reflection.BindingFlags.Instance | System.Reflection.BindingFlags.Public)
|
||||||
|
.FirstOrDefault(p => p.Name == propertyName && p.GetIndexParameters().Length == 0);
|
||||||
|
|
||||||
|
if (prop is null || !prop.CanWrite)
|
||||||
|
return;
|
||||||
|
|
||||||
|
var propertyType = prop.PropertyType;
|
||||||
|
object? value;
|
||||||
|
|
||||||
|
if (!propertyType.IsValueType || Nullable.GetUnderlyingType(propertyType) is not null)
|
||||||
|
{
|
||||||
|
value = null;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
value = Activator.CreateInstance(propertyType);
|
||||||
|
}
|
||||||
|
|
||||||
|
prop.SetValue(target, value);
|
||||||
|
}
|
||||||
|
|
||||||
private sealed record StatusFilterItem(EnvelopeStatus Value, string Text);
|
private sealed record StatusFilterItem(EnvelopeStatus Value, string Text);
|
||||||
}
|
}
|
||||||
@@ -170,7 +170,7 @@
|
|||||||
|
|
||||||
if (result == EnvelopeLoginResult.Success)
|
if (result == EnvelopeLoginResult.Success)
|
||||||
{
|
{
|
||||||
Navigation.NavigateTo($"/envelope/{Uri.EscapeDataString(EnvelopeKey)}", forceLoad: true);
|
Navigation.NavigateTo($"/envelope/{Uri.EscapeDataString(EnvelopeKey)}", forceLoad: false);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -162,7 +162,7 @@
|
|||||||
var result = await AuthService.LoginSenderAsync(Username.Trim(), Password.Trim());
|
var result = await AuthService.LoginSenderAsync(Username.Trim(), Password.Trim());
|
||||||
|
|
||||||
if (result == SenderLoginResult.Success) {
|
if (result == SenderLoginResult.Success) {
|
||||||
Navigation.NavigateTo("/sender", forceLoad: true);
|
Navigation.NavigateTo("/sender", forceLoad: false);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -5,6 +5,7 @@
|
|||||||
@using EnvelopeGenerator.Application.Common.Dto
|
@using EnvelopeGenerator.Application.Common.Dto
|
||||||
@using EnvelopeGenerator.Application.Envelopes.Commands
|
@using EnvelopeGenerator.Application.Envelopes.Commands
|
||||||
@using EnvelopeGenerator.Application.Resources
|
@using EnvelopeGenerator.Application.Resources
|
||||||
|
@using EnvelopeGenerator.Domain.Constants
|
||||||
@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.WebUtilities
|
||||||
@@ -18,6 +19,8 @@
|
|||||||
@inject PdfRenderService PdfRenderService
|
@inject PdfRenderService PdfRenderService
|
||||||
@inject ReceiverService ReceiverService
|
@inject ReceiverService ReceiverService
|
||||||
@inject EnvelopeService EnvelopeService
|
@inject EnvelopeService EnvelopeService
|
||||||
|
@inject EnvelopeTypeService EnvelopeTypeService
|
||||||
|
@inject AuthService AuthService
|
||||||
@inject IStringLocalizer<Resource> UiLocalizer
|
@inject IStringLocalizer<Resource> UiLocalizer
|
||||||
|
|
||||||
<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" />
|
||||||
@@ -94,6 +97,107 @@
|
|||||||
}
|
}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<details style="border:1px solid #dbe5ef;border-radius:8px;background:#f9fbfd;">
|
||||||
|
<summary style="cursor:pointer;list-style:none;display:flex;align-items:center;justify-content:space-between;padding:0.55rem 0.7rem;font-size:0.74rem;font-weight:700;color:#1f2937;text-transform:uppercase;letter-spacing:0.04em;">
|
||||||
|
<span>@L("SenderEditor_EnvelopeSettings", "Envelope-Einstellungen")</span>
|
||||||
|
<span style="font-size:0.66rem;color:#64748b;font-weight:600;">@L("SenderEditor_AdvancedOptional", "Erweitert")</span>
|
||||||
|
</summary>
|
||||||
|
|
||||||
|
<div style="padding:0 0.7rem 0.7rem;display:grid;grid-template-columns:repeat(auto-fit,minmax(180px,1fr));gap:0.45rem;">
|
||||||
|
<div style="display:flex;flex-direction:column;gap:0.2rem;">
|
||||||
|
<label style="font-size:0.65rem;font-weight:600;color:#64748b;">@L("EnvelopeType", "Umschlagtyp")</label>
|
||||||
|
<select value="@(_selectedEnvelopeTypeId?.ToString() ?? string.Empty)"
|
||||||
|
@onchange="OnEnvelopeTypeChanged"
|
||||||
|
style="font-size:0.78rem;border:1px solid #d1d5db;border-radius:5px;padding:0.3rem 0.45rem;background:#fff;">
|
||||||
|
<option value="">@L("SenderEditor_SelectTemplate", "Vorlage auswählen")</option>
|
||||||
|
@foreach (var type in _envelopeTypes)
|
||||||
|
{
|
||||||
|
<option value="@type.Id">@type.Title</option>
|
||||||
|
}
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div style="display:flex;flex-direction:column;gap:0.2rem;">
|
||||||
|
<label style="font-size:0.65rem;font-weight:600;color:#64748b;">@L("Language", "Sprache")</label>
|
||||||
|
<select @bind="_language"
|
||||||
|
style="font-size:0.78rem;border:1px solid #d1d5db;border-radius:5px;padding:0.3rem 0.45rem;background:#fff;">
|
||||||
|
<option value="de">Deutsch (de)</option>
|
||||||
|
<option value="en">English (en)</option>
|
||||||
|
<option value="de-DE">Deutsch (de-DE)</option>
|
||||||
|
<option value="en-US">English (en-US)</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div style="display:flex;flex-direction:column;gap:0.2rem;">
|
||||||
|
<label style="font-size:0.65rem;font-weight:600;color:#64748b;">@L("CertificationType", "Zertifizierung")</label>
|
||||||
|
<select @bind="_certificationType"
|
||||||
|
style="font-size:0.78rem;border:1px solid #d1d5db;border-radius:5px;padding:0.3rem 0.45rem;background:#fff;">
|
||||||
|
<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 style="display:flex;flex-direction:column;gap:0.3rem;justify-content:flex-end;padding-bottom:0.25rem;">
|
||||||
|
<label style="display:flex;align-items:center;gap:0.45rem;font-size:0.74rem;color:#1f2937;">
|
||||||
|
<input type="checkbox" checked="@_tfaEnabled" @onchange="OnTfaChanged" />
|
||||||
|
@L("TFAEnabled", "2FA aktiviert")
|
||||||
|
</label>
|
||||||
|
<label style="display:flex;align-items:center;gap:0.45rem;font-size:0.74rem;color:#1f2937;">
|
||||||
|
<input type="checkbox" checked="@_useAccessCode" @onchange="OnUseAccessCodeChanged" disabled="@_tfaEnabled" />
|
||||||
|
@L("UseAccessCode", "Zugriffscode verwenden")
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div style="display:flex;flex-direction:column;gap:0.2rem;">
|
||||||
|
<label style="font-size:0.65rem;font-weight:600;color:#64748b;">@L("FinalEmailToCreator", "Finale E-Mail an Ersteller")</label>
|
||||||
|
<select @bind="_finalEmailToCreator"
|
||||||
|
style="font-size:0.78rem;border:1px solid #d1d5db;border-radius:5px;padding:0.3rem 0.45rem;background:#fff;">
|
||||||
|
<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 style="display:flex;flex-direction:column;gap:0.2rem;">
|
||||||
|
<label style="font-size:0.65rem;font-weight:600;color:#64748b;">@L("FinalEmailToReceivers", "Finale E-Mail an Empfänger")</label>
|
||||||
|
<select @bind="_finalEmailToReceivers"
|
||||||
|
style="font-size:0.78rem;border:1px solid #d1d5db;border-radius:5px;padding:0.3rem 0.45rem;background:#fff;">
|
||||||
|
<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 style="display:flex;flex-direction:column;gap:0.3rem;justify-content:flex-end;padding-bottom:0.25rem;">
|
||||||
|
<label style="display:flex;align-items:center;gap:0.45rem;font-size:0.74rem;color:#1f2937;">
|
||||||
|
<input type="checkbox" checked="@_sendReminderEmails" @onchange="OnSendReminderEmailsChanged" />
|
||||||
|
@L("SendReminderEmails", "Erinnerungs-E-Mails senden")
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div style="display:flex;flex-direction:column;gap:0.2rem;">
|
||||||
|
<label style="font-size:0.65rem;font-weight:600;color:#64748b;">@L("FirstReminderDays", "Erste Erinnerung (Tage)")</label>
|
||||||
|
<InputNumber @bind-Value="_firstReminderDays" class="form-control form-control-sm" disabled="@(!_sendReminderEmails)" />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div style="display:flex;flex-direction:column;gap:0.2rem;">
|
||||||
|
<label style="font-size:0.65rem;font-weight:600;color:#64748b;">@L("ReminderIntervalDays", "Erinnerungsintervall (Tage)")</label>
|
||||||
|
<InputNumber @bind-Value="_reminderIntervalDays" class="form-control form-control-sm" disabled="@(!_sendReminderEmails)" />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div style="display:flex;flex-direction:column;gap:0.2rem;">
|
||||||
|
<label style="font-size:0.65rem;font-weight:600;color:#64748b;">@L("ExpiresWhenDays", "Läuft ab nach (Tage)")</label>
|
||||||
|
<InputNumber @bind-Value="_expiresWhenDays" class="form-control form-control-sm" />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div style="display:flex;flex-direction:column;gap:0.2rem;">
|
||||||
|
<label style="font-size:0.65rem;font-weight:600;color:#64748b;">@L("ExpiresWarningWhenDays", "Ablaufwarnung (Tage)")</label>
|
||||||
|
<InputNumber @bind-Value="_expiresWarningWhenDays" class="form-control form-control-sm" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</details>
|
||||||
|
|
||||||
<div class="sender-receivers-panel">
|
<div class="sender-receivers-panel">
|
||||||
<div class="sender-receivers-panel__header">
|
<div class="sender-receivers-panel__header">
|
||||||
<div class="sender-receivers-panel__title-wrap">
|
<div class="sender-receivers-panel__title-wrap">
|
||||||
@@ -560,6 +664,20 @@
|
|||||||
string _envelopeMessage = string.Empty;
|
string _envelopeMessage = string.Empty;
|
||||||
bool _titleTouched = false;
|
bool _titleTouched = false;
|
||||||
|
|
||||||
|
List<EnvelopeTypeDto> _envelopeTypes = [];
|
||||||
|
int? _selectedEnvelopeTypeId;
|
||||||
|
string _language = "de-DE";
|
||||||
|
bool _useAccessCode;
|
||||||
|
bool _tfaEnabled;
|
||||||
|
int _certificationType = (int)CertificationType.AdvancedElectronicSignature;
|
||||||
|
int _finalEmailToCreator = (int)FinalEmailType.No;
|
||||||
|
int _finalEmailToReceivers = (int)FinalEmailType.No;
|
||||||
|
bool _sendReminderEmails;
|
||||||
|
int _firstReminderDays;
|
||||||
|
int _reminderIntervalDays;
|
||||||
|
int _expiresWhenDays;
|
||||||
|
int _expiresWarningWhenDays;
|
||||||
|
|
||||||
List<ReceiverDraft> _receivers = [];
|
List<ReceiverDraft> _receivers = [];
|
||||||
bool _receiverPopupVisible;
|
bool _receiverPopupVisible;
|
||||||
string _receiverDraftName = string.Empty;
|
string _receiverDraftName = string.Empty;
|
||||||
@@ -577,6 +695,32 @@
|
|||||||
|
|
||||||
// ── Lifecycle ──
|
// ── Lifecycle ──
|
||||||
|
|
||||||
|
protected override async Task OnInitializedAsync()
|
||||||
|
{
|
||||||
|
var hasAccess = await AuthService.CheckSenderAccessAsync();
|
||||||
|
if (!hasAccess)
|
||||||
|
{
|
||||||
|
NavigationManager.NavigateTo("/sender/login");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
_envelopeTypes = (await EnvelopeTypeService.GetAsync()).ToList();
|
||||||
|
|
||||||
|
if (!IsEditMode && _selectedEnvelopeTypeId is null && _envelopeTypes.Count > 0)
|
||||||
|
{
|
||||||
|
_selectedEnvelopeTypeId = _envelopeTypes[0].Id;
|
||||||
|
ApplyEnvelopeTypeDefaults(_envelopeTypes[0]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
Logger.LogWarning(ex, "Failed to load envelope types for sender editor.");
|
||||||
|
_envelopeTypes = [];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
protected override async Task OnParametersSetAsync()
|
protected override async Task OnParametersSetAsync()
|
||||||
{
|
{
|
||||||
if (EnvelopeId.HasValue && EnvelopeId.Value > 0)
|
if (EnvelopeId.HasValue && EnvelopeId.Value > 0)
|
||||||
@@ -621,6 +765,24 @@
|
|||||||
|
|
||||||
_envelopeTitle = envelope.Title ?? string.Empty;
|
_envelopeTitle = envelope.Title ?? string.Empty;
|
||||||
_envelopeMessage = envelope.Message ?? string.Empty;
|
_envelopeMessage = envelope.Message ?? string.Empty;
|
||||||
|
_selectedEnvelopeTypeId = envelope.EnvelopeTypeId;
|
||||||
|
_language = string.IsNullOrWhiteSpace(envelope.Language) ? "de-DE" : envelope.Language;
|
||||||
|
_useAccessCode = envelope.UseAccessCode;
|
||||||
|
_tfaEnabled = envelope.TFAEnabled;
|
||||||
|
if (_tfaEnabled)
|
||||||
|
_useAccessCode = true;
|
||||||
|
|
||||||
|
_certificationType = envelope.CertificationType ?? (int)CertificationType.AdvancedElectronicSignature;
|
||||||
|
_finalEmailToCreator = envelope.FinalEmailToCreator ?? (int)FinalEmailType.No;
|
||||||
|
_finalEmailToReceivers = envelope.FinalEmailToReceivers ?? (int)FinalEmailType.No;
|
||||||
|
|
||||||
|
_sendReminderEmails = envelope.SendReminderEmails ?? envelope.EnvelopeType?.SendReminderEmails ?? false;
|
||||||
|
_firstReminderDays = envelope.FirstReminderDays ?? envelope.EnvelopeType?.FirstReminderDays ?? 0;
|
||||||
|
_reminderIntervalDays = envelope.ReminderIntervalDays ?? envelope.EnvelopeType?.ReminderIntervalDays ?? 0;
|
||||||
|
_expiresWhenDays = envelope.ExpiresWhenDays ?? envelope.EnvelopeType?.ExpiresDays ?? 0;
|
||||||
|
_expiresWarningWhenDays = envelope.ExpiresWarningWhenDays ?? envelope.EnvelopeType?.ExpiresWarningDays ?? 0;
|
||||||
|
|
||||||
|
NormalizeSettings();
|
||||||
|
|
||||||
var envelopeReceivers = envelope.EnvelopeReceivers?.ToList() ?? [];
|
var envelopeReceivers = envelope.EnvelopeReceivers?.ToList() ?? [];
|
||||||
_receivers = envelopeReceivers
|
_receivers = envelopeReceivers
|
||||||
@@ -664,11 +826,31 @@
|
|||||||
|
|
||||||
_originalPdfBytes = document?.ByteData;
|
_originalPdfBytes = document?.ByteData;
|
||||||
_pdfLoaded = _originalPdfBytes is { Length: > 0 };
|
_pdfLoaded = _originalPdfBytes is { Length: > 0 };
|
||||||
|
|
||||||
|
if (_pdfLoaded)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
_pagePreviews = (await PdfRenderService.RenderPagesAsync(_originalPdfBytes!))
|
||||||
|
.OrderBy(p => p.PageNumber)
|
||||||
|
.ToList();
|
||||||
|
}
|
||||||
|
catch (Exception renderEx)
|
||||||
|
{
|
||||||
|
Logger.LogWarning(renderEx, "Failed to render PDF page previews for envelope {EnvelopeId}. Falling back to native preview.", envelopeId);
|
||||||
_pagePreviews = [];
|
_pagePreviews = [];
|
||||||
|
}
|
||||||
|
|
||||||
_pageCount = _pagePreviews.Count > 0
|
_pageCount = _pagePreviews.Count > 0
|
||||||
? _pagePreviews.Count
|
? _pagePreviews.Count
|
||||||
: EstimatePdfPageCount(_originalPdfBytes)
|
: 1;
|
||||||
?? Math.Max(1, _signatureFields.Select(f => f.Page).DefaultIfEmpty(1).Max());
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
_pagePreviews = [];
|
||||||
|
_pageCount = Math.Max(1, _signatureFields.Select(f => f.Page).DefaultIfEmpty(1).Max());
|
||||||
|
}
|
||||||
|
|
||||||
_fileName = $"Envelope-{envelopeId}.pdf";
|
_fileName = $"Envelope-{envelopeId}.pdf";
|
||||||
_pendingReceiverForPlacement = null;
|
_pendingReceiverForPlacement = null;
|
||||||
_loadedEnvelopeId = envelopeId;
|
_loadedEnvelopeId = envelopeId;
|
||||||
@@ -755,6 +937,90 @@
|
|||||||
|
|
||||||
void CancelPlacement() => _pendingReceiverForPlacement = null;
|
void CancelPlacement() => _pendingReceiverForPlacement = null;
|
||||||
|
|
||||||
|
Task OnEnvelopeTypeChanged(ChangeEventArgs e)
|
||||||
|
{
|
||||||
|
var raw = e.Value?.ToString();
|
||||||
|
if (!int.TryParse(raw, out var id) || id <= 0)
|
||||||
|
{
|
||||||
|
_selectedEnvelopeTypeId = null;
|
||||||
|
return Task.CompletedTask;
|
||||||
|
}
|
||||||
|
|
||||||
|
_selectedEnvelopeTypeId = id;
|
||||||
|
var selectedType = _envelopeTypes.FirstOrDefault(x => x.Id == id);
|
||||||
|
if (selectedType is not null)
|
||||||
|
ApplyEnvelopeTypeDefaults(selectedType);
|
||||||
|
|
||||||
|
return Task.CompletedTask;
|
||||||
|
}
|
||||||
|
|
||||||
|
void ApplyEnvelopeTypeDefaults(EnvelopeTypeDto envelopeType)
|
||||||
|
{
|
||||||
|
if (!string.IsNullOrWhiteSpace(envelopeType.Language))
|
||||||
|
_language = envelopeType.Language;
|
||||||
|
|
||||||
|
if (envelopeType.UseAccessCode.HasValue)
|
||||||
|
_useAccessCode = envelopeType.UseAccessCode.Value;
|
||||||
|
|
||||||
|
if (_tfaEnabled)
|
||||||
|
_useAccessCode = true;
|
||||||
|
|
||||||
|
if (envelopeType.CertificationType.HasValue)
|
||||||
|
_certificationType = envelopeType.CertificationType.Value;
|
||||||
|
|
||||||
|
if (envelopeType.FinalEmailToCreator.HasValue)
|
||||||
|
_finalEmailToCreator = envelopeType.FinalEmailToCreator.Value;
|
||||||
|
|
||||||
|
if (envelopeType.FinalEmailToReceivers.HasValue)
|
||||||
|
_finalEmailToReceivers = envelopeType.FinalEmailToReceivers.Value;
|
||||||
|
|
||||||
|
if (envelopeType.SendReminderEmails.HasValue)
|
||||||
|
_sendReminderEmails = envelopeType.SendReminderEmails.Value;
|
||||||
|
|
||||||
|
if (envelopeType.FirstReminderDays.HasValue)
|
||||||
|
_firstReminderDays = envelopeType.FirstReminderDays.Value;
|
||||||
|
|
||||||
|
if (envelopeType.ReminderIntervalDays.HasValue)
|
||||||
|
_reminderIntervalDays = envelopeType.ReminderIntervalDays.Value;
|
||||||
|
|
||||||
|
if (envelopeType.ExpiresDays.HasValue)
|
||||||
|
_expiresWhenDays = envelopeType.ExpiresDays.Value;
|
||||||
|
|
||||||
|
if (envelopeType.ExpiresWarningDays.HasValue)
|
||||||
|
_expiresWarningWhenDays = envelopeType.ExpiresWarningDays.Value;
|
||||||
|
}
|
||||||
|
|
||||||
|
void OnTfaChanged(ChangeEventArgs e)
|
||||||
|
{
|
||||||
|
_tfaEnabled = ParseCheckboxValue(e);
|
||||||
|
if (_tfaEnabled)
|
||||||
|
_useAccessCode = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
void OnUseAccessCodeChanged(ChangeEventArgs e)
|
||||||
|
{
|
||||||
|
if (_tfaEnabled)
|
||||||
|
{
|
||||||
|
_useAccessCode = true;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
_useAccessCode = ParseCheckboxValue(e);
|
||||||
|
}
|
||||||
|
|
||||||
|
void OnSendReminderEmailsChanged(ChangeEventArgs e)
|
||||||
|
{
|
||||||
|
_sendReminderEmails = ParseCheckboxValue(e);
|
||||||
|
}
|
||||||
|
|
||||||
|
static bool ParseCheckboxValue(ChangeEventArgs e)
|
||||||
|
{
|
||||||
|
if (e.Value is bool b)
|
||||||
|
return b;
|
||||||
|
|
||||||
|
return bool.TryParse(e.Value?.ToString(), out var parsed) && parsed;
|
||||||
|
}
|
||||||
|
|
||||||
// ── PDF area click → place field ──
|
// ── PDF area click → place field ──
|
||||||
async Task OnPdfAreaClickAsync(MouseEventArgs e)
|
async Task OnPdfAreaClickAsync(MouseEventArgs e)
|
||||||
{
|
{
|
||||||
@@ -872,6 +1138,13 @@
|
|||||||
|
|
||||||
async Task SaveDraftAsync()
|
async Task SaveDraftAsync()
|
||||||
{
|
{
|
||||||
|
if (!ValidateSettings(out var settingsError))
|
||||||
|
{
|
||||||
|
_saveErrorMessage = settingsError;
|
||||||
|
_savePopupVisible = true;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
_isSaving = true;
|
_isSaving = true;
|
||||||
_saveErrorMessage = null;
|
_saveErrorMessage = null;
|
||||||
await InvokeAsync(StateHasChanged);
|
await InvokeAsync(StateHasChanged);
|
||||||
@@ -912,6 +1185,13 @@
|
|||||||
// ── Send envelope ──
|
// ── Send envelope ──
|
||||||
async Task SendAsync()
|
async Task SendAsync()
|
||||||
{
|
{
|
||||||
|
if (!ValidateSettings(out var settingsError))
|
||||||
|
{
|
||||||
|
_saveErrorMessage = settingsError;
|
||||||
|
_savePopupVisible = true;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
// ── Validation ──
|
// ── Validation ──
|
||||||
_titleTouched = true;
|
_titleTouched = true;
|
||||||
if (!_pdfLoaded || _originalPdfBytes is null)
|
if (!_pdfLoaded || _originalPdfBytes is null)
|
||||||
@@ -999,6 +1279,8 @@
|
|||||||
|
|
||||||
CreateEnvelopeCommand BuildCreateEnvelopeCommand(bool send)
|
CreateEnvelopeCommand BuildCreateEnvelopeCommand(bool send)
|
||||||
{
|
{
|
||||||
|
NormalizeSettings();
|
||||||
|
|
||||||
var envelopeId = _editingEnvelopeId
|
var envelopeId = _editingEnvelopeId
|
||||||
?? EnvelopeId
|
?? EnvelopeId
|
||||||
?? _loadedEnvelopeId
|
?? _loadedEnvelopeId
|
||||||
@@ -1031,15 +1313,68 @@
|
|||||||
return new CreateEnvelopeCommand
|
return new CreateEnvelopeCommand
|
||||||
{
|
{
|
||||||
EnvelopeId = envelopeId,
|
EnvelopeId = envelopeId,
|
||||||
|
EnvelopeTypeId = _selectedEnvelopeTypeId,
|
||||||
Title = _envelopeTitle,
|
Title = _envelopeTitle,
|
||||||
Message = _envelopeMessage,
|
Message = _envelopeMessage,
|
||||||
TFAEnabled = false,
|
Language = _language,
|
||||||
|
UseAccessCode = _useAccessCode,
|
||||||
|
TFAEnabled = _tfaEnabled,
|
||||||
|
CertificationType = _certificationType,
|
||||||
|
FinalEmailToCreator = _finalEmailToCreator,
|
||||||
|
FinalEmailToReceivers = _finalEmailToReceivers,
|
||||||
|
SendReminderEmails = _sendReminderEmails,
|
||||||
|
FirstReminderDays = _firstReminderDays,
|
||||||
|
ReminderIntervalDays = _reminderIntervalDays,
|
||||||
|
ExpiresWhenDays = _expiresWhenDays,
|
||||||
|
ExpiresWarningWhenDays = _expiresWarningWhenDays,
|
||||||
Send = send,
|
Send = send,
|
||||||
Document = string.IsNullOrWhiteSpace(docBase64) ? null : new CreateEnvelopeDocumentDto(docBase64),
|
Document = string.IsNullOrWhiteSpace(docBase64) ? null : new CreateEnvelopeDocumentDto(docBase64),
|
||||||
Receivers = receiverCommands
|
Receivers = receiverCommands
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void NormalizeSettings()
|
||||||
|
{
|
||||||
|
_language = string.IsNullOrWhiteSpace(_language) ? "de-DE" : _language.Trim();
|
||||||
|
|
||||||
|
if (_tfaEnabled)
|
||||||
|
_useAccessCode = true;
|
||||||
|
|
||||||
|
_firstReminderDays = Math.Max(0, _firstReminderDays);
|
||||||
|
_reminderIntervalDays = Math.Max(0, _reminderIntervalDays);
|
||||||
|
_expiresWhenDays = Math.Max(0, _expiresWhenDays);
|
||||||
|
_expiresWarningWhenDays = Math.Max(0, _expiresWarningWhenDays);
|
||||||
|
|
||||||
|
if (_expiresWhenDays > 0 && _expiresWarningWhenDays > _expiresWhenDays)
|
||||||
|
_expiresWarningWhenDays = _expiresWhenDays;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool ValidateSettings(out string? message)
|
||||||
|
{
|
||||||
|
NormalizeSettings();
|
||||||
|
|
||||||
|
if (_sendReminderEmails && _reminderIntervalDays <= 0)
|
||||||
|
{
|
||||||
|
message = L("SenderEditor_Validation_ReminderInterval", "Bitte geben Sie ein Erinnerungsintervall groesser als 0 Tage ein.");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (_sendReminderEmails && _firstReminderDays < 0)
|
||||||
|
{
|
||||||
|
message = L("SenderEditor_Validation_FirstReminder", "Bitte geben Sie gueltige Tage fuer die erste Erinnerung ein.");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (_expiresWhenDays > 0 && _expiresWarningWhenDays > _expiresWhenDays)
|
||||||
|
{
|
||||||
|
message = L("SenderEditor_Validation_ExpiryWarning", "Die Ablaufwarnung darf nicht nach dem Ablaufdatum liegen.");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
message = null;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
int? GetEnvelopeIdFromCurrentUri()
|
int? GetEnvelopeIdFromCurrentUri()
|
||||||
{
|
{
|
||||||
try
|
try
|
||||||
|
|||||||
@@ -41,6 +41,7 @@ builder.Services.AddScoped<SignatureService>();
|
|||||||
builder.Services.AddScoped<SignatureCacheService>();
|
builder.Services.AddScoped<SignatureCacheService>();
|
||||||
builder.Services.AddSingleton<AppVersionService>();
|
builder.Services.AddSingleton<AppVersionService>();
|
||||||
builder.Services.AddScoped<EnvelopeService>();
|
builder.Services.AddScoped<EnvelopeService>();
|
||||||
|
builder.Services.AddScoped<EnvelopeTypeService>();
|
||||||
builder.Services.AddScoped<LocalizationService>();
|
builder.Services.AddScoped<LocalizationService>();
|
||||||
builder.Services.AddScoped<ReceiverService>();
|
builder.Services.AddScoped<ReceiverService>();
|
||||||
builder.Services.AddScoped<PdfRenderService>();
|
builder.Services.AddScoped<PdfRenderService>();
|
||||||
|
|||||||
@@ -26,4 +26,24 @@ public class AnnotationService(IHttpClientFactory httpClientFactory)
|
|||||||
var result = await response.Content.ReadFromJsonAsync<List<AnnotationDto>>(_jsonOptions, cancel);
|
var result = await response.Content.ReadFromJsonAsync<List<AnnotationDto>>(_jsonOptions, cancel);
|
||||||
return result ?? [];
|
return result ?? [];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public async Task RejectAsync(string envelopeKey, string? reason, CancellationToken cancel = default)
|
||||||
|
{
|
||||||
|
using var http = httpClientFactory.CreateClient("EnvelopeGenerator.Server");
|
||||||
|
|
||||||
|
var response = await http.PostAsJsonAsync(
|
||||||
|
$"/api/Annotation/reject/{Uri.EscapeDataString(envelopeKey)}",
|
||||||
|
reason,
|
||||||
|
_jsonOptions,
|
||||||
|
cancel);
|
||||||
|
|
||||||
|
if (!response.IsSuccessStatusCode)
|
||||||
|
{
|
||||||
|
var body = await response.Content.ReadAsStringAsync(cancel);
|
||||||
|
throw new HttpRequestException(
|
||||||
|
$"Failed to reject envelope {envelopeKey}: {(int)response.StatusCode} {response.StatusCode} - {body}",
|
||||||
|
null,
|
||||||
|
response.StatusCode);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -10,6 +10,15 @@ public class DocumentService(IHttpClientFactory httpClientFactory)
|
|||||||
{
|
{
|
||||||
private static readonly JsonSerializerOptions _jsonOptions = new(JsonSerializerDefaults.Web);
|
private static readonly JsonSerializerOptions _jsonOptions = new(JsonSerializerDefaults.Web);
|
||||||
|
|
||||||
|
private static HttpRequestException CreateHttpException(string messagePrefix, HttpStatusCode statusCode, string? reasonPhrase)
|
||||||
|
{
|
||||||
|
var statusCodeInt = (int)statusCode;
|
||||||
|
return new HttpRequestException(
|
||||||
|
$"{messagePrefix}. Status: {statusCodeInt} ({reasonPhrase ?? "Unknown error"})",
|
||||||
|
null,
|
||||||
|
statusCode);
|
||||||
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Fetches the PDF bytes for the given envelope key from the API.
|
/// Fetches the PDF bytes for the given envelope key from the API.
|
||||||
/// Throws HttpRequestException on failure with appropriate status code.
|
/// Throws HttpRequestException on failure with appropriate status code.
|
||||||
@@ -21,14 +30,7 @@ public class DocumentService(IHttpClientFactory httpClientFactory)
|
|||||||
var response = await http.GetAsync($"/api/Document/{Uri.EscapeDataString(envelopeKey)}", cancel);
|
var response = await http.GetAsync($"/api/Document/{Uri.EscapeDataString(envelopeKey)}", cancel);
|
||||||
|
|
||||||
if (!response.IsSuccessStatusCode)
|
if (!response.IsSuccessStatusCode)
|
||||||
{
|
throw CreateHttpException("Failed to load document", response.StatusCode, response.ReasonPhrase);
|
||||||
var statusCode = (int)response.StatusCode;
|
|
||||||
var reasonPhrase = response.ReasonPhrase ?? "Unknown error";
|
|
||||||
throw new HttpRequestException(
|
|
||||||
$"Failed to load document. Status: {statusCode} ({reasonPhrase})",
|
|
||||||
null,
|
|
||||||
response.StatusCode);
|
|
||||||
}
|
|
||||||
|
|
||||||
var bytes = await response.Content.ReadAsByteArrayAsync(cancel);
|
var bytes = await response.Content.ReadAsByteArrayAsync(cancel);
|
||||||
return bytes;
|
return bytes;
|
||||||
@@ -36,23 +38,67 @@ public class DocumentService(IHttpClientFactory httpClientFactory)
|
|||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Fetches sender document details (including signature elements) by envelope id.
|
/// Fetches sender document details (including signature elements) by envelope id.
|
||||||
/// Uses GET /api/Document?EnvelopeId={envelopeId}.
|
/// Uses GET /api/Document/details?EnvelopeId={envelopeId}.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public async Task<DocumentDto?> GetSenderDocumentDetailsByEnvelopeIdAsync(int envelopeId, CancellationToken cancel = default)
|
public async Task<DocumentDto?> GetSenderDocumentDetailsByEnvelopeIdAsync(int envelopeId, CancellationToken cancel = default)
|
||||||
|
{
|
||||||
|
using var http = httpClientFactory.CreateClient("EnvelopeGenerator.Server");
|
||||||
|
var detailsResponse = await http.GetAsync($"/api/Document/details?EnvelopeId={envelopeId}", cancel);
|
||||||
|
|
||||||
|
if (detailsResponse.StatusCode == HttpStatusCode.NotFound)
|
||||||
|
{
|
||||||
|
var fallbackBytes = await TryGetSenderDocumentBytesByEnvelopeIdAsync(http, envelopeId, cancel);
|
||||||
|
if (fallbackBytes is null)
|
||||||
|
return null;
|
||||||
|
|
||||||
|
return new DocumentDto(fallbackBytes, new DocumentDetailsDto
|
||||||
|
{
|
||||||
|
EnvelopeId = envelopeId,
|
||||||
|
AddedWhen = DateTime.UtcNow,
|
||||||
|
Elements = []
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!detailsResponse.IsSuccessStatusCode)
|
||||||
|
throw CreateHttpException("Failed to load sender document details", detailsResponse.StatusCode, detailsResponse.ReasonPhrase);
|
||||||
|
|
||||||
|
var details = await detailsResponse.Content.ReadFromJsonAsync<DocumentDetailsDto>(_jsonOptions, cancel)
|
||||||
|
?? throw new HttpRequestException("Failed to deserialize sender document details response.");
|
||||||
|
|
||||||
|
var byteData = await TryGetSenderDocumentBytesByEnvelopeIdAsync(http, envelopeId, cancel);
|
||||||
|
if (byteData is null)
|
||||||
|
byteData = [];
|
||||||
|
|
||||||
|
return new DocumentDto(byteData, details);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Fetches sender document bytes by envelope id.
|
||||||
|
/// Uses GET /api/Document?EnvelopeId={envelopeId}.
|
||||||
|
/// </summary>
|
||||||
|
public async Task<byte[]?> GetSenderDocumentBytesByEnvelopeIdAsync(int envelopeId, CancellationToken cancel = default)
|
||||||
{
|
{
|
||||||
using var http = httpClientFactory.CreateClient("EnvelopeGenerator.Server");
|
using var http = httpClientFactory.CreateClient("EnvelopeGenerator.Server");
|
||||||
var response = await http.GetAsync($"/api/Document?EnvelopeId={envelopeId}", cancel);
|
var response = await http.GetAsync($"/api/Document?EnvelopeId={envelopeId}", cancel);
|
||||||
|
|
||||||
|
if (response.StatusCode == HttpStatusCode.NotFound)
|
||||||
|
return null;
|
||||||
|
|
||||||
if (!response.IsSuccessStatusCode)
|
if (!response.IsSuccessStatusCode)
|
||||||
{
|
throw CreateHttpException("Failed to load sender document bytes", response.StatusCode, response.ReasonPhrase);
|
||||||
var statusCode = (int)response.StatusCode;
|
|
||||||
var reasonPhrase = response.ReasonPhrase ?? "Unknown error";
|
return await response.Content.ReadAsByteArrayAsync(cancel);
|
||||||
throw new HttpRequestException(
|
|
||||||
$"Failed to load sender document details. Status: {statusCode} ({reasonPhrase})",
|
|
||||||
null,
|
|
||||||
response.StatusCode);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return await response.Content.ReadFromJsonAsync<DocumentDto>(_jsonOptions, cancel);
|
private static async Task<byte[]?> TryGetSenderDocumentBytesByEnvelopeIdAsync(HttpClient http, int envelopeId, CancellationToken cancel)
|
||||||
|
{
|
||||||
|
var bytesResponse = await http.GetAsync($"/api/Document?EnvelopeId={envelopeId}", cancel);
|
||||||
|
if (bytesResponse.StatusCode == HttpStatusCode.NotFound)
|
||||||
|
return null;
|
||||||
|
|
||||||
|
if (!bytesResponse.IsSuccessStatusCode)
|
||||||
|
throw CreateHttpException("Failed to load sender document bytes", bytesResponse.StatusCode, bytesResponse.ReasonPhrase);
|
||||||
|
|
||||||
|
return await bytesResponse.Content.ReadAsByteArrayAsync(cancel);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -101,26 +101,7 @@ public class EnvelopeService(IHttpClientFactory clientFactory)
|
|||||||
CancellationToken cancel = default)
|
CancellationToken cancel = default)
|
||||||
{
|
{
|
||||||
var httpClient = clientFactory.CreateClient("EnvelopeGenerator.Server");
|
var httpClient = clientFactory.CreateClient("EnvelopeGenerator.Server");
|
||||||
var payload = new
|
var response = await httpClient.PostAsJsonAsync("/api/Envelope", request, _jsonOptions, cancel);
|
||||||
{
|
|
||||||
EnvelopeId = request.EnvelopeId,
|
|
||||||
Title = request.Title,
|
|
||||||
Message = request.Message,
|
|
||||||
TFAEnabled = request.TFAEnabled,
|
|
||||||
Send = request.Send,
|
|
||||||
Document = request.Document is null
|
|
||||||
? null
|
|
||||||
: new { DataAsBase64 = request.Document.DataAsBase64 },
|
|
||||||
Receivers = request.Receivers.Select(r => new
|
|
||||||
{
|
|
||||||
EmailAddress = r.EmailAddress,
|
|
||||||
Name = r.Name,
|
|
||||||
PhoneNumber = r.PhoneNumber,
|
|
||||||
Fields = r.Fields.Select(f => new { X = f.X, Y = f.Y, Page = f.Page }).ToList()
|
|
||||||
}).ToList()
|
|
||||||
};
|
|
||||||
|
|
||||||
var response = await httpClient.PostAsJsonAsync("/api/Envelope", payload, _jsonOptions, cancel);
|
|
||||||
|
|
||||||
if (response.IsSuccessStatusCode)
|
if (response.IsSuccessStatusCode)
|
||||||
return await response.Content.ReadFromJsonAsync<EnvelopeDto>(_jsonOptions, cancel);
|
return await response.Content.ReadFromJsonAsync<EnvelopeDto>(_jsonOptions, cancel);
|
||||||
|
|||||||
@@ -0,0 +1,22 @@
|
|||||||
|
using EnvelopeGenerator.Application.Common.Dto;
|
||||||
|
using System.Net.Http.Json;
|
||||||
|
using System.Text.Json;
|
||||||
|
|
||||||
|
namespace EnvelopeGenerator.Server.Client.Services;
|
||||||
|
|
||||||
|
public class EnvelopeTypeService(IHttpClientFactory clientFactory)
|
||||||
|
{
|
||||||
|
private static readonly JsonSerializerOptions JsonOptions = new(JsonSerializerDefaults.Web);
|
||||||
|
|
||||||
|
public async Task<IReadOnlyList<EnvelopeTypeDto>> GetAsync(CancellationToken cancel = default)
|
||||||
|
{
|
||||||
|
var http = clientFactory.CreateClient("EnvelopeGenerator.Server");
|
||||||
|
var response = await http.GetAsync("/api/EnvelopeType", cancel);
|
||||||
|
|
||||||
|
if (!response.IsSuccessStatusCode)
|
||||||
|
return [];
|
||||||
|
|
||||||
|
var data = await response.Content.ReadFromJsonAsync<List<EnvelopeTypeDto>>(JsonOptions, cancel);
|
||||||
|
return data ?? [];
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -22,6 +22,7 @@
|
|||||||
@inject EnvelopeGenerator.Server.Client.Services.AuthService AuthService
|
@inject EnvelopeGenerator.Server.Client.Services.AuthService AuthService
|
||||||
@inject EnvelopeGenerator.Server.Services.EnvelopeReceiverAuthorizationService ReceiverAuthorizationService
|
@inject EnvelopeGenerator.Server.Services.EnvelopeReceiverAuthorizationService ReceiverAuthorizationService
|
||||||
@inject EnvelopeGenerator.Server.Services.EnvelopeReceiverPageDataService PageDataService
|
@inject EnvelopeGenerator.Server.Services.EnvelopeReceiverPageDataService PageDataService
|
||||||
|
@inject AnnotationService AnnotationService
|
||||||
@inject AppVersionService AppVersion
|
@inject AppVersionService AppVersion
|
||||||
@inject IMemoryCache MemoryCache
|
@inject IMemoryCache MemoryCache
|
||||||
@inject ILogger<ReceiverPage> Logger
|
@inject ILogger<ReceiverPage> Logger
|
||||||
@@ -132,32 +133,6 @@
|
|||||||
<span class="pdf-toolbar__btn-text">Unterschreiben</span>
|
<span class="pdf-toolbar__btn-text">Unterschreiben</span>
|
||||||
</button>
|
</button>
|
||||||
}
|
}
|
||||||
|
|
||||||
@if (!(_envelopeReceiver?.Envelope?.ReadOnly ?? false))
|
|
||||||
{
|
|
||||||
<button class="pdf-toolbar__btn"
|
|
||||||
@onclick="OpenSharePopup"
|
|
||||||
title="Dokument teilen"
|
|
||||||
style="flex-shrink: 0;">
|
|
||||||
<svg xmlns="http://www.w3.org/2000/svg" width="14" height="14" fill="currentColor" viewBox="0 0 16 16">
|
|
||||||
<path d="M13.5 1a1.5 1.5 0 1 0 1.415 2H15a1.5 1.5 0 0 0-1.5-2zm-11 5a1.5 1.5 0 1 0 1.415 2H4a1.5 1.5 0 0 0-1.5-2zm11 5a1.5 1.5 0 1 0 1.415 2H15a1.5 1.5 0 0 0-1.5-2z"/>
|
|
||||||
<path d="M2.854 7.646a.5.5 0 0 1 .0-.707l9-5a.5.5 0 1 1 .486.874l-9 5a.5.5 0 0 1-.486-.167zm0 .708a.5.5 0 0 1 .486-.167l9 5a.5.5 0 0 1-.486.874l-9-5a.5.5 0 0 1 0-.707z"/>
|
|
||||||
</svg>
|
|
||||||
<span class="pdf-toolbar__btn-text">Teilen</span>
|
|
||||||
</button>
|
|
||||||
}
|
|
||||||
|
|
||||||
<button class="pdf-toolbar__btn"
|
|
||||||
@onclick="LogoutAsync"
|
|
||||||
disabled="@_isLoggingOut"
|
|
||||||
title="Abmelden"
|
|
||||||
style="flex-shrink: 0;">
|
|
||||||
<svg xmlns="http://www.w3.org/2000/svg" width="14" height="14" fill="currentColor" viewBox="0 0 16 16">
|
|
||||||
<path fill-rule="evenodd" d="M10 15a1 1 0 0 0 1-1v-2a.5.5 0 0 1 1 0v2a2 2 0 0 1-2 2H3a2 2 0 0 1-2-2V2a2 2 0 0 1 2-2h7a2 2 0 0 1 2 2v2a.5.5 0 0 1-1 0V2a1 1 0 0 0-1-1H3a1 1 0 0 0-1 1v12a1 1 0 0 0 1 1h7z" />
|
|
||||||
<path fill-rule="evenodd" d="M15.854 8.354a.5.5 0 0 0 0-.708l-3-3a.5.5 0 1 0-.708.708L14.293 7.5H6.5a.5.5 0 0 0 0 1h7.793l-2.147 2.146a.5.5 0 0 0 .708.708l3-3z" />
|
|
||||||
</svg>
|
|
||||||
<span class="pdf-toolbar__btn-text">@(_isLoggingOut ? "Abmelden..." : "Abmelden")</span>
|
|
||||||
</button>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -375,57 +350,6 @@
|
|||||||
</FooterContentTemplate>
|
</FooterContentTemplate>
|
||||||
</DxPopup>
|
</DxPopup>
|
||||||
|
|
||||||
<DxPopup @bind-Visible="_sharePopupVisible"
|
|
||||||
HeaderText="Dokument teilen"
|
|
||||||
Width="520px"
|
|
||||||
MaxWidth="95vw"
|
|
||||||
ShowFooter="true"
|
|
||||||
CloseOnOutsideClick="true">
|
|
||||||
<BodyContentTemplate>
|
|
||||||
<div class="mb-3">
|
|
||||||
<label class="form-label" for="rp-share-mail" style="font-size: 0.9rem; font-weight: 500;">E-Mail</label>
|
|
||||||
<input id="rp-share-mail"
|
|
||||||
class="form-control"
|
|
||||||
placeholder="user@mail.com"
|
|
||||||
value="@_shareReceiverMail"
|
|
||||||
@oninput="args => _shareReceiverMail = args.Value?.ToString() ?? string.Empty" />
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="mb-1">
|
|
||||||
<label class="form-label" for="rp-share-date" style="font-size: 0.9rem; font-weight: 500;">Gultig bis</label>
|
|
||||||
<input id="rp-share-date"
|
|
||||||
class="form-control"
|
|
||||||
type="date"
|
|
||||||
min="@DateTime.Today.AddDays(1).ToString("yyyy-MM-dd")"
|
|
||||||
max="@DateTime.Today.AddDays(90).ToString("yyyy-MM-dd")"
|
|
||||||
value="@_shareDateValid.ToString("yyyy-MM-dd")"
|
|
||||||
@oninput="OnShareDateChanged" />
|
|
||||||
</div>
|
|
||||||
|
|
||||||
@if (!string.IsNullOrWhiteSpace(_shareMessage))
|
|
||||||
{
|
|
||||||
<div class="mt-3 alert @(_shareMessageIsError ? "alert-danger" : "alert-success") mb-0" role="alert">
|
|
||||||
@_shareMessage
|
|
||||||
</div>
|
|
||||||
}
|
|
||||||
</BodyContentTemplate>
|
|
||||||
<FooterContentTemplate>
|
|
||||||
<div class="d-flex justify-content-end gap-2 w-100">
|
|
||||||
<button class="btn btn-outline-secondary"
|
|
||||||
@onclick="CloseSharePopup"
|
|
||||||
disabled="@_isSharing">
|
|
||||||
Schliessen
|
|
||||||
</button>
|
|
||||||
<button class="btn btn-primary"
|
|
||||||
@onclick="ShareEnvelopeAsync"
|
|
||||||
disabled="@_isSharing"
|
|
||||||
style="background: linear-gradient(135deg, #4F46E5 0%, #4338CA 100%); border: none;">
|
|
||||||
@(_isSharing ? "Senden..." : "Senden")
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</FooterContentTemplate>
|
|
||||||
</DxPopup>
|
|
||||||
|
|
||||||
@code {
|
@code {
|
||||||
// ----- Constants -----
|
// ----- Constants -----
|
||||||
const string SignatureTabDraw = "draw";
|
const string SignatureTabDraw = "draw";
|
||||||
@@ -480,6 +404,12 @@
|
|||||||
string? _shareMessage;
|
string? _shareMessage;
|
||||||
bool _shareMessageIsError = false;
|
bool _shareMessageIsError = false;
|
||||||
|
|
||||||
|
// ----- Reject popup state -----
|
||||||
|
bool _rejectPopupVisible = false;
|
||||||
|
bool _isRejectSubmitting = false;
|
||||||
|
string _rejectReason = string.Empty;
|
||||||
|
string? _rejectValidationMessage;
|
||||||
|
|
||||||
async Task LogoutAsync()
|
async Task LogoutAsync()
|
||||||
{
|
{
|
||||||
if (_isLoggingOut || string.IsNullOrWhiteSpace(EnvelopeKey))
|
if (_isLoggingOut || string.IsNullOrWhiteSpace(EnvelopeKey))
|
||||||
@@ -552,23 +482,12 @@
|
|||||||
|
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
var shared = await AuthService.ShareEnvelopeReadOnlyAsync(EnvelopeKey!, email, _shareDateValid.Date);
|
_shareMessage = "Freigabe ist in dieser Ansicht derzeit nicht verfugbar.";
|
||||||
if (shared)
|
|
||||||
{
|
|
||||||
_shareMessage = "Dokument wurde erfolgreich als Nur-Lesen freigegeben.";
|
|
||||||
_shareMessageIsError = false;
|
_shareMessageIsError = false;
|
||||||
_shareReceiverMail = string.Empty;
|
|
||||||
_shareDateValid = DateTime.Today.AddDays(7);
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
_shareMessage = "Freigabe fehlgeschlagen. Bitte versuchen Sie es erneut.";
|
|
||||||
_shareMessageIsError = true;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
catch (Exception ex)
|
catch (Exception ex)
|
||||||
{
|
{
|
||||||
Logger.LogWarning(ex, "Failed to share envelope in read-only mode for {EnvelopeKey}", EnvelopeKey);
|
Logger.LogError(ex, "Failed to share envelope {EnvelopeKey}", EnvelopeKey);
|
||||||
_shareMessage = "Freigabe fehlgeschlagen. Bitte versuchen Sie es erneut.";
|
_shareMessage = "Freigabe fehlgeschlagen. Bitte versuchen Sie es erneut.";
|
||||||
_shareMessageIsError = true;
|
_shareMessageIsError = true;
|
||||||
}
|
}
|
||||||
@@ -627,6 +546,11 @@
|
|||||||
_envelopeReceiver = await PageDataService.GetEnvelopeReceiverAsync(EnvelopeKey);
|
_envelopeReceiver = await PageDataService.GetEnvelopeReceiverAsync(EnvelopeKey);
|
||||||
if (_envelopeReceiver is null)
|
if (_envelopeReceiver is null)
|
||||||
Logger.LogWarning("Envelope receiver data is null for {EnvelopeKey}", EnvelopeKey);
|
Logger.LogWarning("Envelope receiver data is null for {EnvelopeKey}", EnvelopeKey);
|
||||||
|
else if (IsRejected(_envelopeReceiver))
|
||||||
|
{
|
||||||
|
Navigation.NavigateTo($"/envelope/{Uri.EscapeDataString(EnvelopeKey)}/rejected", forceLoad: true);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
// Build initial report (no signature image yet)
|
// Build initial report (no signature image yet)
|
||||||
_report = BuildReport(_pdfBytes, _signatures, capturedSignature: null);
|
_report = BuildReport(_pdfBytes, _signatures, capturedSignature: null);
|
||||||
@@ -672,6 +596,16 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
// ----- Report builder -----
|
// ----- Report builder -----
|
||||||
|
static bool IsRejected(EnvelopeGenerator.Application.Common.Dto.EnvelopeReceiver.EnvelopeReceiverDto envelopeReceiver)
|
||||||
|
{
|
||||||
|
var status = envelopeReceiver.Envelope?.Status;
|
||||||
|
if (status is EnvelopeStatus.DocumentRejected or EnvelopeStatus.EnvelopeRejected)
|
||||||
|
return true;
|
||||||
|
|
||||||
|
return envelopeReceiver.Envelope?.Histories?.Any(h =>
|
||||||
|
h.Status is EnvelopeStatus.DocumentRejected or EnvelopeStatus.EnvelopeRejected) == true;
|
||||||
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Builds an XtraReport wrapping the PDF bytes.
|
/// Builds an XtraReport wrapping the PDF bytes.
|
||||||
/// If a signature is captured and there are signature fields, the signature image is
|
/// If a signature is captured and there are signature fields, the signature image is
|
||||||
@@ -905,6 +839,56 @@
|
|||||||
forceLoad: true);
|
forceLoad: true);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void OpenRejectPopup()
|
||||||
|
{
|
||||||
|
_rejectValidationMessage = null;
|
||||||
|
_rejectPopupVisible = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
void CloseRejectPopup()
|
||||||
|
{
|
||||||
|
if (_isRejectSubmitting)
|
||||||
|
return;
|
||||||
|
|
||||||
|
_rejectPopupVisible = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
async Task SubmitRejectAsync()
|
||||||
|
{
|
||||||
|
if (_isRejectSubmitting)
|
||||||
|
return;
|
||||||
|
|
||||||
|
_isRejectSubmitting = true;
|
||||||
|
_rejectValidationMessage = null;
|
||||||
|
await InvokeAsync(StateHasChanged);
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var reason = string.IsNullOrWhiteSpace(_rejectReason)
|
||||||
|
? null
|
||||||
|
: _rejectReason.Trim();
|
||||||
|
|
||||||
|
if (string.IsNullOrWhiteSpace(EnvelopeKey))
|
||||||
|
{
|
||||||
|
_rejectValidationMessage = "Envelope-Schluessel fehlt.";
|
||||||
|
_isRejectSubmitting = false;
|
||||||
|
await InvokeAsync(StateHasChanged);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
await AnnotationService.RejectAsync(EnvelopeKey, reason);
|
||||||
|
|
||||||
|
Navigation.NavigateTo($"/envelope/login/{Uri.EscapeDataString(EnvelopeKey)}", forceLoad: true);
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
Logger.LogError(ex, "Failed to reject envelope {EnvelopeKey}", EnvelopeKey);
|
||||||
|
_rejectValidationMessage = "Die Ablehnung konnte nicht gespeichert werden. Bitte versuchen Sie es erneut.";
|
||||||
|
_isRejectSubmitting = false;
|
||||||
|
await InvokeAsync(StateHasChanged);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
async Task<string?> GetActiveSignatureDataUrlAsync()
|
async Task<string?> GetActiveSignatureDataUrlAsync()
|
||||||
{
|
{
|
||||||
if (_activeSignatureTab == SignatureTabDraw)
|
if (_activeSignatureTab == SignatureTabDraw)
|
||||||
|
|||||||
@@ -0,0 +1,105 @@
|
|||||||
|
@page "/envelope/{EnvelopeKey}/rejected"
|
||||||
|
@rendermode InteractiveServer
|
||||||
|
@using EnvelopeGenerator.Application.Resources
|
||||||
|
@using EnvelopeGenerator.Domain.Constants
|
||||||
|
@using EnvelopeGenerator.Domain.Interfaces
|
||||||
|
@using Microsoft.Extensions.Localization
|
||||||
|
@inject NavigationManager Navigation
|
||||||
|
@inject EnvelopeGenerator.Server.Services.EnvelopeReceiverAuthorizationService ReceiverAuthorizationService
|
||||||
|
@inject EnvelopeGenerator.Server.Services.EnvelopeReceiverPageDataService PageDataService
|
||||||
|
@inject IStringLocalizer<Resource> UiLocalizer
|
||||||
|
|
||||||
|
<div class="envelope-viewer-layout" style="justify-content: center; align-items: center; padding: 1.5rem;">
|
||||||
|
<div style="width: min(920px, 100%); background: rgba(255,255,255,0.96); border-radius: 20px; box-shadow: 0 18px 48px rgba(15,23,42,0.22); overflow: hidden;">
|
||||||
|
<div style="padding: 1.2rem 1.4rem; border-bottom: 1px solid rgba(15,23,42,0.08); background: linear-gradient(135deg, rgba(239,68,68,0.12) 0%, rgba(220,38,38,0.08) 100%);">
|
||||||
|
<div style="font-size: 1rem; font-weight: 700; color: #991b1b;">@L("DocRejected", "Dokument abgelehnt")</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div style="padding: 2rem 1.5rem;">
|
||||||
|
<div style="display: flex; justify-content: center; margin-bottom: 1rem;">
|
||||||
|
<div style="width: 90px; height: 90px; border-radius: 999px; background: #fee2e2; display: inline-flex; align-items: center; justify-content: center; color: #b91c1c;">
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" width="48" height="48" fill="currentColor" viewBox="0 0 16 16">
|
||||||
|
<path d="M16 8A8 8 0 1 1 0 8a8 8 0 0 1 16 0zM5.354 4.646a.5.5 0 1 0-.708.708L7.293 8l-2.647 2.646a.5.5 0 0 0 .708.708L8 8.707l2.646 2.647a.5.5 0 0 0 .708-.708L8.707 8l2.647-2.646a.5.5 0 0 0-.708-.708L8 7.293 5.354 4.646z" />
|
||||||
|
</svg>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<h2 style="text-align: center; font-size: 1.4rem; color: #7f1d1d; margin-bottom: 0.75rem;">
|
||||||
|
@(_isReadAndConfirm
|
||||||
|
? L("RejectionInfo1Confirmation", "Dieses Dokument wurde als nicht bestaetigt markiert.")
|
||||||
|
: L("RejectionInfo1", "Dieses Dokument wurde abgelehnt."))
|
||||||
|
</h2>
|
||||||
|
|
||||||
|
<p style="text-align: center; margin: 0 auto; max-width: 760px; color: #374151; line-height: 1.6;">
|
||||||
|
@((MarkupString)_messageLine2)
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<div style="margin-top: 1.4rem; text-align: center;">
|
||||||
|
<a href="/" class="btn btn-outline-secondary" style="font-weight: 600;">Portal Startseite</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
@code {
|
||||||
|
[Parameter] public string? EnvelopeKey { get; set; }
|
||||||
|
|
||||||
|
bool _isReadAndConfirm;
|
||||||
|
string _messageLine2 = string.Empty;
|
||||||
|
|
||||||
|
protected override async Task OnInitializedAsync()
|
||||||
|
{
|
||||||
|
if (string.IsNullOrWhiteSpace(EnvelopeKey))
|
||||||
|
{
|
||||||
|
Navigation.NavigateTo("/");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
var user = await ReceiverAuthorizationService.AuthorizeAsync(EnvelopeKey);
|
||||||
|
if (user is null)
|
||||||
|
{
|
||||||
|
Navigation.NavigateTo($"/envelope/login/{Uri.EscapeDataString(EnvelopeKey)}");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
var envelopeReceiver = await PageDataService.GetEnvelopeReceiverAsync(EnvelopeKey);
|
||||||
|
if (envelopeReceiver is null)
|
||||||
|
{
|
||||||
|
Navigation.NavigateTo($"/envelope/{Uri.EscapeDataString(EnvelopeKey)}", forceLoad: true);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!IsRejected(envelopeReceiver))
|
||||||
|
{
|
||||||
|
Navigation.NavigateTo($"/envelope/{Uri.EscapeDataString(EnvelopeKey)}", forceLoad: true);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
_isReadAndConfirm = envelopeReceiver.Envelope?.IsReadAndConfirm() ?? false;
|
||||||
|
|
||||||
|
var senderName = envelopeReceiver.Envelope?.User is null
|
||||||
|
? string.Empty
|
||||||
|
: $"{envelopeReceiver.Envelope.User.Prename} {envelopeReceiver.Envelope.User.Name}".Trim();
|
||||||
|
var senderEmail = envelopeReceiver.Envelope?.User?.Email ?? string.Empty;
|
||||||
|
var envelopeTitle = envelopeReceiver.Envelope?.Title ?? string.Empty;
|
||||||
|
|
||||||
|
var template = L("RejectionInfo2", "Das Dokument \"{2}\" von {0} ({1}) wurde abgelehnt.");
|
||||||
|
_messageLine2 = string.Format(template, senderName, senderEmail, envelopeTitle);
|
||||||
|
}
|
||||||
|
|
||||||
|
static bool IsRejected(EnvelopeGenerator.Application.Common.Dto.EnvelopeReceiver.EnvelopeReceiverDto envelopeReceiver)
|
||||||
|
{
|
||||||
|
var status = envelopeReceiver.Envelope?.Status;
|
||||||
|
if (status is EnvelopeStatus.DocumentRejected or EnvelopeStatus.EnvelopeRejected)
|
||||||
|
return true;
|
||||||
|
|
||||||
|
return envelopeReceiver.Envelope?.Histories?.Any(h =>
|
||||||
|
h.Status is EnvelopeStatus.DocumentRejected or EnvelopeStatus.EnvelopeRejected) == true;
|
||||||
|
}
|
||||||
|
|
||||||
|
string L(string key, string fallback)
|
||||||
|
{
|
||||||
|
var value = UiLocalizer[key];
|
||||||
|
return value.ResourceNotFound ? fallback : value.Value;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -100,11 +100,12 @@ public class AnnotationController : ControllerBase
|
|||||||
/// <summary>
|
/// <summary>
|
||||||
/// Rejects the document for the current receiver.
|
/// Rejects the document for the current receiver.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
|
/// <param name="envelopeKey">Envelope key route parameter.</param>
|
||||||
/// <param name="reason">Optional rejection reason.</param>
|
/// <param name="reason">Optional rejection reason.</param>
|
||||||
[Authorize(Policy = AuthPolicy.Receiver)]
|
[Authorize(Policy = AuthPolicy.Receiver)]
|
||||||
[HttpPost("reject")]
|
[HttpPost("reject/{envelopeKey}")]
|
||||||
[Obsolete("Use MediatR")]
|
[Obsolete("Use MediatR")]
|
||||||
public async Task<IActionResult> Reject([FromBody] string? reason = null)
|
public async Task<IActionResult> Reject([FromRoute] string envelopeKey, [FromBody] string? reason = null)
|
||||||
{
|
{
|
||||||
var signature = User.ReceiverSignature();
|
var signature = User.ReceiverSignature();
|
||||||
var uuid = User.EnvelopeUuid();
|
var uuid = User.EnvelopeUuid();
|
||||||
|
|||||||
@@ -31,17 +31,30 @@ public class DocumentController(IMediator mediator, IAuthorizationService authSe
|
|||||||
/// <param name="cancel">Cancellation token.</param>
|
/// <param name="cancel">Cancellation token.</param>
|
||||||
[HttpGet]
|
[HttpGet]
|
||||||
[Authorize(AuthenticationSchemes = AuthScheme.Sender)]
|
[Authorize(AuthenticationSchemes = AuthScheme.Sender)]
|
||||||
public async Task<IActionResult> GetDocument(CancellationToken cancel, [FromQuery] ReadDocumentQuery? query = null)
|
public async Task<IActionResult> GetDocument(CancellationToken cancel, [FromQuery] ReadDocumentQuery query)
|
||||||
{
|
{
|
||||||
if (query is null)
|
|
||||||
return BadRequest("Missing document query.");
|
|
||||||
|
|
||||||
var senderDoc = await mediator.Send(query, cancel);
|
var senderDoc = await mediator.Send(query, cancel);
|
||||||
return senderDoc.ByteData is byte[] senderDocByte
|
return senderDoc?.ByteData is byte[] senderDocByte
|
||||||
? File(senderDocByte, "application/octet-stream")
|
? File(senderDocByte, "application/octet-stream")
|
||||||
: NotFound("Document is empty.");
|
: NotFound("Document is empty.");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Returns sender document metadata and element details (without binary payload) for editor loading.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="query">Document query with EnvelopeId.</param>
|
||||||
|
/// <param name="cancel">Cancellation token.</param>
|
||||||
|
[HttpGet("details")]
|
||||||
|
[Authorize(AuthenticationSchemes = AuthScheme.Sender)]
|
||||||
|
public async Task<IActionResult> GetDocumentDetails(CancellationToken cancel, [FromQuery] ReadDocumentDetailsQuery query)
|
||||||
|
{
|
||||||
|
var senderDoc = await mediator.Send(query with { UserId = User.GetId() }, cancel);
|
||||||
|
if (senderDoc is null)
|
||||||
|
return NotFound("Document is empty.");
|
||||||
|
|
||||||
|
return Ok(senderDoc);
|
||||||
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Gets the document for the specified envelope key.
|
/// Gets the document for the specified envelope key.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
|
|||||||
@@ -11,6 +11,7 @@ namespace EnvelopeGenerator.Server.Controllers;
|
|||||||
[Route("api/[controller]")]
|
[Route("api/[controller]")]
|
||||||
public class PdfRenderController(IPdfPageImageRenderer renderer) : ControllerBase
|
public class PdfRenderController(IPdfPageImageRenderer renderer) : ControllerBase
|
||||||
{
|
{
|
||||||
|
// 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)]
|
||||||
public async Task<IActionResult> RenderPages([FromBody] PdfRenderRequestDto? request, CancellationToken cancel)
|
public async Task<IActionResult> RenderPages([FromBody] PdfRenderRequestDto? request, CancellationToken cancel)
|
||||||
|
|||||||
@@ -67,7 +67,7 @@ public static class ReceiverClaimExtensions
|
|||||||
/// Gets the authenticated receiver email address from the claims.
|
/// Gets the authenticated receiver email address from the claims.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public static string ReceiverMail(this ClaimsPrincipal user)
|
public static string ReceiverMail(this ClaimsPrincipal user)
|
||||||
=> user.GetRequiredClaimValue(JwtRegisteredClaimNames.Email);
|
=> user.GetRequiredClaimValue(JwtRegisteredClaimNames.Email, ClaimTypes.Email);
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Gets the authenticated envelope identifier from the claims.
|
/// Gets the authenticated envelope identifier from the claims.
|
||||||
|
|||||||
@@ -345,6 +345,7 @@ try
|
|||||||
builder.Services.AddScoped<EnvelopeGenerator.Server.Client.Services.LocalizationService>();
|
builder.Services.AddScoped<EnvelopeGenerator.Server.Client.Services.LocalizationService>();
|
||||||
builder.Services.AddScoped<EnvelopeGenerator.Server.Client.Services.ReceiverService>();
|
builder.Services.AddScoped<EnvelopeGenerator.Server.Client.Services.ReceiverService>();
|
||||||
builder.Services.AddScoped<EnvelopeGenerator.Server.Client.Services.PdfRenderService>();
|
builder.Services.AddScoped<EnvelopeGenerator.Server.Client.Services.PdfRenderService>();
|
||||||
|
builder.Services.AddScoped<EnvelopeGenerator.Server.Client.Services.EnvelopeTypeService>();
|
||||||
|
|
||||||
// Business Services (Server specific)
|
// Business Services (Server specific)
|
||||||
builder.Services.AddScoped<DocumentService>();
|
builder.Services.AddScoped<DocumentService>();
|
||||||
@@ -353,7 +354,7 @@ try
|
|||||||
builder.Services.AddScoped<EnvelopeReceiverService>();
|
builder.Services.AddScoped<EnvelopeReceiverService>();
|
||||||
builder.Services.AddScoped<SignatureService>();
|
builder.Services.AddScoped<SignatureService>();
|
||||||
builder.Services.AddScoped<SignatureCacheService>();
|
builder.Services.AddScoped<SignatureCacheService>();
|
||||||
builder.Services.AddScoped<IPdfPageImageRenderer, PdfPageImageRenderer>();
|
builder.Services.AddDocInfrastructure();
|
||||||
builder.Services.AddScoped<EnvelopeGenerator.Application.Common.Interfaces.Services.IEnvelopeMailService, EnvelopeGenerator.Application.Services.EnvelopeMailService>();
|
builder.Services.AddScoped<EnvelopeGenerator.Application.Common.Interfaces.Services.IEnvelopeMailService, EnvelopeGenerator.Application.Services.EnvelopeMailService>();
|
||||||
builder.Services.AddSingleton<AppVersionService>();
|
builder.Services.AddSingleton<AppVersionService>();
|
||||||
|
|
||||||
|
|||||||
@@ -57,6 +57,14 @@ public class EnvelopeReceiverPageDataService(
|
|||||||
return await mediator.Send(new ReadDocumentQuery(EnvelopeId: envelopeId, UserId: user.SenderId()), cancellationToken);
|
return await mediator.Send(new ReadDocumentQuery(EnvelopeId: envelopeId, UserId: user.SenderId()), cancellationToken);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Loads document metadata/elements for the authenticated sender without binary payload.
|
||||||
|
/// </summary>
|
||||||
|
public async Task<DocumentDetailsDto?> GetDocumentMetadataOfSenderAsync(ClaimsPrincipal user, int envelopeId, CancellationToken cancellationToken = default)
|
||||||
|
{
|
||||||
|
return await mediator.Send(new ReadDocumentDetailsQuery(EnvelopeId: envelopeId, UserId: user.SenderId()), cancellationToken);
|
||||||
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Loads the current receiver's signature placeholders.
|
/// Loads the current receiver's signature placeholders.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
|
|||||||
@@ -954,6 +954,29 @@ body.resizing {
|
|||||||
cursor: not-allowed;
|
cursor: not-allowed;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.pdf-toolbar__btn--reject {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 0.375rem;
|
||||||
|
min-width: auto;
|
||||||
|
padding: 0.5rem 0.75rem;
|
||||||
|
background: linear-gradient(135deg, rgba(239, 68, 68, 0.08) 0%, rgba(220, 38, 38, 0.08) 100%);
|
||||||
|
border: 1px solid rgba(239, 68, 68, 0.3);
|
||||||
|
color: #b91c1c;
|
||||||
|
}
|
||||||
|
|
||||||
|
.pdf-toolbar__btn--reject:hover:not(:disabled) {
|
||||||
|
background: linear-gradient(135deg, #ef4444 0%, #dc2626 100%);
|
||||||
|
border-color: transparent;
|
||||||
|
color: #ffffff;
|
||||||
|
box-shadow: 0 4px 12px rgba(239, 68, 68, 0.3);
|
||||||
|
}
|
||||||
|
|
||||||
|
.pdf-toolbar__btn--reject:disabled {
|
||||||
|
opacity: 0.6;
|
||||||
|
cursor: not-allowed;
|
||||||
|
}
|
||||||
|
|
||||||
.pdf-toolbar__btn-text {
|
.pdf-toolbar__btn-text {
|
||||||
font-size: 0.813rem;
|
font-size: 0.813rem;
|
||||||
font-weight: 600;
|
font-weight: 600;
|
||||||
@@ -1591,6 +1614,18 @@ body.resizing {
|
|||||||
color: #a5b4fc;
|
color: #a5b4fc;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[data-sf-theme="blazing-dark"] .pdf-toolbar__btn--reject {
|
||||||
|
background: rgba(239, 68, 68, 0.15);
|
||||||
|
border-color: rgba(248, 113, 113, 0.4);
|
||||||
|
color: #fca5a5;
|
||||||
|
}
|
||||||
|
|
||||||
|
[data-sf-theme="blazing-dark"] .pdf-toolbar__btn--reject:hover:not(:disabled) {
|
||||||
|
background: rgba(239, 68, 68, 0.35);
|
||||||
|
border-color: rgba(248, 113, 113, 0.6);
|
||||||
|
color: #ffffff;
|
||||||
|
}
|
||||||
|
|
||||||
/* Error container */
|
/* Error container */
|
||||||
[data-sf-theme="blazing-dark"] .error-container .alert-danger {
|
[data-sf-theme="blazing-dark"] .error-container .alert-danger {
|
||||||
background: rgba(239, 68, 68, 0.12);
|
background: rgba(239, 68, 68, 0.12);
|
||||||
|
|||||||
Reference in New Issue
Block a user