Compare commits

..

73 Commits

Author SHA1 Message Date
7cb1546934 Add Reject endpoint to AnnotationController
Introduced a new Reject endpoint for users with the ReceiverFull role to reject annotations, with optional reason support. Marked the endpoint as obsolete for future exception handling updates. Also standardized the Unauthorized response to remove the custom error message.
2026-03-11 12:09:23 +01:00
60db762bcc Bump project version to 3.12.2
Updated EnvelopeGenerator.Web.csproj to increment <Version>, <AssemblyVersion>, and <FileVersion> from 3.12.1 to 3.12.2, reflecting the new release. No other changes were made.
2026-03-11 12:08:48 +01:00
5e840db04c Refactor envelope rejection logic in EnvelopeController
Clarified and streamlined the handling of rejected envelopes:
- Moved retrieval of rejecting receivers for clarity.
- Signed out users when any rejecting receivers are present.
- Improved determination of "external" users.
- Updated condition for showing the EnvelopeRejected view.
- Temporarily hardcoded ViewBag.IsExt with a TODO for future fix.
- Removed redundant code for better maintainability.
2026-03-11 12:05:20 +01:00
e724a74f9c Bump version to 3.12.1 in project file
Updated <Version>, <AssemblyVersion>, and <FileVersion> in EnvelopeGenerator.Web.csproj from 3.12.0 to 3.12.1 to reflect a new patch release. No other changes were made.
2026-03-11 11:19:17 +01:00
48b7afcdc1 Restrict rejection check to ReadAndSign envelopes
Previously, rejection logic was applied to all envelopes. Now, the check for rejected receivers and the "EnvelopeRejected" view are only executed if the envelope is of the "ReadAndSign" type, preventing unnecessary rejection handling for other envelope types.
2026-03-11 11:16:08 +01:00
717da90c01 Add .NET 7.0 support and update project dependencies
Added net7.0 to TargetFrameworks in EnvelopeGenerator.Web.csproj and introduced a conditional ItemGroup with required package references for .NET 7.0 compatibility. Also removed clipboard2.svg from the list of included static files.
2026-03-11 11:07:33 +01:00
8054bb377d Refine rejection check for read-and-sign envelopes
Only return 423 Locked if envelope is read-and-sign and has a rejection history. Also add missing using for Domain.Interfaces in AnnotationController.
2026-03-11 09:43:35 +01:00
200258ff73 Update EnvelopeDto.ReadOnly to use IsReadAndConfirm()
Refactored the ReadOnly property in EnvelopeDto to use the IsReadAndConfirm() extension method instead of directly checking EnvelopeTypeId. Marked the property as [Obsolete] and advised using EnvelopeExtensions.IsReadAndConfirm.
2026-03-11 09:43:22 +01:00
fa73d939b5 Only check rejection for read-and-sign envelopes
Previously, rejection checks were performed for all envelopes. Now, the code checks for rejecting receivers only if the envelope is of the "read-and-sign" type, avoiding unnecessary queries and logic for other envelope types.
2026-03-11 09:29:24 +01:00
ca9e25abcb Add IsReadAndSign extension method to IEnvelope
Introduced the IsReadAndSign method to IEnvelope extensions, which returns true when EnvelopeTypeId is not 2. This allows clear differentiation between "Read and Sign" and "Read and Confirm" envelope types.
2026-03-11 09:28:40 +01:00
82831991b0 Merge branch 'master' of http://git.dd:3000/AppStd/EnvelopeGenerator 2026-03-10 13:21:13 +01:00
OlgunR
260e8d53ba Update 'SignatureConfirmed' localization strings
Changed 'SignatureConfirmed' value in English to "Finalization confirmed" and in German to "Abschluss bestätigt". Updated designer file comments to match new wording.
2026-03-10 12:45:56 +01:00
OlgunR
0fd174ee0c Update resource strings and comments for read confirmation in Domain.Resources.Model
Switched Model.Designer.cs comments from German to English and added new resource string properties for read confirmation workflows. Updated Model.resx and Model.en.resx with new and corrected entries, improved translation consistency, and clarified documentation.
2026-03-10 11:55:57 +01:00
OlgunR
ab4cd7c254 Update resources for confirmation workflows & English comments in CommonService.Strings.Model
Replaced German comments/descriptions with English for clarity and internationalization. Added new resource strings for confirmation/read confirmation statuses (e.g., CompletelyConfirmed, Confirmation, DocumentConfirmed, etc.) to support both signing and read confirmation workflows. Corrected and standardized several existing resource values for consistency across languages.
2026-03-10 10:54:52 +01:00
1f5468b1ac Refactor PDF canvas transform to use Save/Restore state
Move Y-axis flip transform inside signature drawing block and
wrap with SaveState/RestoreState to prevent side effects on
other drawing operations and pages. Removes redundant
page-level transformation for better encapsulation.
2026-03-10 04:07:37 +01:00
b20aafe7a5 Fix PDF/A conversion status check in PDFMerger
Previously, the code checked the status of the original document
after converting the merged PDF to PDF/A, which could miss
conversion errors. Now, it correctly checks the status of the
merged PDF to ensure the conversion succeeded.
2026-03-10 04:07:28 +01:00
466d0a3a7a Refactor envelope and annotation data retrieval logic
Add private helper functions ReadEnvelope and LoadAnnotationDataForEnvelope to centralize retrieval of envelope byte data and annotation JSON from the database. Update PDF annotation burning logic to use these helpers, improving code reuse and testability.
2026-03-10 04:07:14 +01:00
7281cb47c3 Remove duplicate ProjectReference in BBTests.vbproj
Eliminated a redundant ProjectReference to EnvelopeGenerator.CommonServices.vbproj from the EnvelopeGenerator.BBTests.vbproj file, ensuring only a single reference remains. This helps prevent potential build issues and maintains project file clarity.
2026-03-10 03:25:38 +01:00
eb5db3d6be Remove StatusChangedWhen from DocumentStatus entity
The StatusChangedWhen property, previously required and mapped
to the "STATUS_CHANGED_WHEN" column, has been removed from
the DocumentStatus entity in the EnvelopeGenerator.Domain.Entities
namespace. This change simplifies the entity and its mapping.
2026-03-10 02:44:12 +01:00
8a534b84d0 Update NuGet packages and binding redirects to latest versions
Updated NuGet package references and assembly binding redirects in App.config, EnvelopeGenerator.Service.vbproj, and packages.config to newer versions compatible with .NET 4.8/4.6.2/4.6.3. No application logic changes; these updates ensure dependency consistency and improved framework compatibility.
2026-03-10 02:17:17 +01:00
c523153654 Add "Full Finalize Test" button for PDF finalization debug
Added a new "Full Finalize Test" button to frmFinalizePDF, along with its event handler. The handler loads annotation data, document bytes, and related entities, logs detailed information about the envelope and signatures, and tests the PDF annotation burning process. The result is saved to the desktop and opened automatically. Also added necessary repository/entity imports for EF Core access. This feature aids in debugging and verifying the full PDF finalization workflow.
2026-03-10 02:16:58 +01:00
82c85643c8 Add EF Core, SqlClient, and Microsoft.Extensions packages
Updated .vbproj and packages.config to include references and NuGet packages for Microsoft.Data.SqlClient, Entity Framework Core, AutoMapper, and various Microsoft.Extensions libraries (configuration, logging, caching, options). Added import and error checks for SqlClient SNI targets. No source code changes; all updates are related to project and package management.
2026-03-10 01:45:46 +01:00
69892d566c Add System.ComponentModel.Annotations reference to project
Added a reference to System.ComponentModel.Annotations v4.2.1.0 in EnvelopeGenerator.Service.vbproj, specifying the HintPath to the appropriate DLL in the packages directory. This enables use of additional data annotation attributes in the project.
2026-03-10 01:23:26 +01:00
2f41348c59 Add System.ComponentModel.Annotations NuGet package
Added System.ComponentModel.Annotations v4.7.0 to support data validation and metadata attributes in .NET Framework 4.8 projects.
2026-03-10 01:23:06 +01:00
0d56ac7448 Downgrade several NuGet package dependencies
Reverted multiple packages in packages.config to earlier versions, including EntityFramework, System.Text.Json, System.Buffers, and others. These downgrades address compatibility issues and ensure alignment with other project dependencies.
2026-03-10 00:02:32 +01:00
18a563ecd1 Improve annotation handling and date retrieval robustness
Refactored annotation processing in PDFBurner.vb to handle null elements and annotations safely, preventing possible errors and improving code clarity. Updated GetSignedDate in ReceiverModel.vb to ensure consistent DateTime return values, handle DBNull and type conversions, and improve error handling.
2026-03-10 00:02:22 +01:00
73df248d15 Update System.Text.Json to 8.0.6 and enhance ClickOnce config
- Upgraded System.Text.Json NuGet package and binding redirects to 8.0.6.
- Updated project references to use System.Text.Json 8.0.0.6.
- Enhanced ClickOnce publishing settings: set PublishUrl, enabled autorun, completed wizard, and added manifest signing with a temporary key.
- Minor formatting and property order adjustments in the project file.
2026-03-09 22:50:55 +01:00
7c7674c822 Show "Confirmed by" or "Signed by" label conditionally
The signature label now displays "Confirmed by" if READ_AND_CONFIRM is true, otherwise "Signed by". This uses the appropriate localized string when available, defaulting to "Signed by" if not. Previously, only "Signed by" was shown.
2026-03-06 12:55:30 +01:00
65f606f573 Update finalize dialog text based on READ_AND_CONFIRM flag
The confirmation dialog when finalizing a document now displays
context-appropriate text depending on the READ_AND_CONFIRM flag.
If true, it shows localized.confirmAgree; otherwise, it shows
localized.sigAgree, improving clarity for different workflows.
2026-03-06 12:55:19 +01:00
0341505f8d Merge branch 'origin/bugfix/history-inconsistency' 2026-03-06 10:15:22 +01:00
d4eee1718e Bump version to 3.12.0 in project file
Updated <Version>, <AssemblyVersion>, and <FileVersion> fields in EnvelopeGenerator.Web.csproj from 3.11.0 to 3.12.0. No other changes were made.
2026-03-06 10:13:45 +01:00
9b042d8f45 Merge branch 'master' of http://git.dd:3000/AppStd/EnvelopeGenerator 2026-03-06 09:54:26 +01:00
OlgunR
ad0c847172 Add SigningProcessTitle resource; rename rejection key
Added localized SigningProcessTitle entry to de-DE, en-US, and fr-FR resource files. Renamed RejectionInfoConfirmation to RejectionInfo1Confirmation. Removed duplicate SigningProcessTitle entries. No changes to localized values.
2026-03-06 09:53:18 +01:00
f6d57b1e38 Update progress bar to show Confirmations or Signatures
Progress bar label now displays "Confirmations" if isReadAndConfirm is true, otherwise it shows "Signatures". This improves clarity for users based on the envelope's required action.
2026-03-06 09:52:12 +01:00
b64d2b7478 Refactor: cache IsReadAndConfirm() result in variable
Store envelope.IsReadAndConfirm() in isReadAndConfirm variable to avoid redundant calls and improve code readability when setting ViewData["Title"]. No change to logic or behavior.
2026-03-06 09:50:33 +01:00
OlgunR
f8c7f60cf9 Add resource strings for document confirmation workflows
Added new localized strings for document confirmation processes in de-DE, en-US, and fr-FR resource files, including confirmation messages, UI labels, and process titles. Fixed a typo in the German resource and ensured consistency for the "SigningProcessTitle" key across languages.
2026-03-06 09:50:21 +01:00
44edef8ba1 Update envelope view title logic for confirm state
Refined the logic for setting ViewData["Title"] in ShowEnvelope.cshtml. Now, if the envelope requires read and confirm, the title displays "Confirm Document" instead of just "Sign Document" or "View Document", providing clearer context for users.
2026-03-06 09:43:16 +01:00
647c5d2353 Add localization extensions for confirmation resources
Added three new extension methods to the Extensions class in Resource.cs: ConfirmDoc, ConfirmAgree, and ConfirmationProcessTitle. Each method retrieves the localized value for its respective resource key and includes XML documentation, consistent with existing localization methods.
2026-03-06 09:31:19 +01:00
4ce1d2a370 Add RejectionInfo1ForConfirmation extension method
Introduced a new extension method, RejectionInfo1ForConfirmation, to the Extensions class in Resource.cs. This method retrieves the localized string for "RejectionInfo1ForConfirmation" from an IStringLocalizer instance, providing functionality similar to the existing RejectionInfo1 method.
2026-03-06 09:28:12 +01:00
bcc53bf9f1 Update rejection header for read and confirm envelopes
Add logic to display a specific rejection message when an envelope is rejected as part of a "read and confirm" process. The header now distinguishes between external, confirmation-related, and default rejection scenarios for improved user feedback.
2026-03-06 09:27:41 +01:00
f1e38e3bd3 Reverse IsReadAndConfirm logic for envelope status texts
Swapped the display logic for localized titles and messages based on the IsReadAndConfirm flag. Now, "confirmed" texts are shown when IsReadAndConfirm is true, and "signed" texts when false. This update ensures correct status messaging throughout the envelope confirmation flow.
2026-03-06 09:17:17 +01:00
e095860b17 Update EnvelopeSigned page to handle confirm vs sign
Add conditional logic to EnvelopeSigned.cshtml to display different headings and confirmation messages based on whether the document was signed or confirmed, using the IsReadAndConfirm flag. This improves user feedback by distinguishing between signing and confirming actions.
2026-03-06 01:25:07 +01:00
9cfc74aa88 Refactor title logic and fix localizer syntax in view
Refactored EnvelopeSigned.cshtml to use a local variable for IsReadAndConfirm when setting the page title, improving readability. Also updated Razor syntax for localizer calls in <h1> and <p> elements to ensure correct evaluation and formatting.
2026-03-06 01:23:06 +01:00
7cd6ca3a5f Update EnvelopeSigned view model and title logic
Set model to EnvelopeReceiverDto and import required types. Update ViewData["Title"] to use DocSigned or DocConfirmed based on Envelope.IsReadAndConfirm() result.
2026-03-06 01:20:47 +01:00
9b660cb25a Pass model to EnvelopeSigned view on already signed check
Previously, the "EnvelopeSigned" view was returned without a model when an envelope was already signed. Now, the er model object is passed to the view to provide additional context or data.
2026-03-06 01:20:25 +01:00
3d43d1896d Add DocConfirmed extension method to Resource.cs
Added a new DocConfirmed extension method to the Extensions class in Resource.cs. This method enables retrieval of the "DocConfirmed" localized string via IStringLocalizer, similar to the existing DocSigned method.
2026-03-06 01:20:12 +01:00
bcc17f6def Bump version to 3.11.0 in EnvelopeGenerator.Web.csproj
Updated <Version>, <AssemblyVersion>, and <FileVersion> properties from 3.10.0 to 3.11.0 to reflect the new release. No other changes were made.
2026-03-04 16:49:07 +01:00
8e3c334fa3 Update final status to 'Lesebestätigung' for read confirm
Changed the [FINAL_STATUS] placeholder value from "Bestätigung" to the more specific "Lesebestätigung" when an envelope is read and confirmed, improving clarity in status messaging.
2026-03-04 16:19:45 +01:00
08299451bb Customize email placeholders by envelope action type
Added logic to set email template placeholders based on whether the envelope requires "Read and Confirm" or "Sign" actions. Placeholders such as [SIGNATURE_TYPE], [DOCUMENT_PROCESS], [FINAL_STATUS], [FINAL_ACTION], [REJECTED_BY_OTHERS], and [RECEIVER_ACTION] are now dynamically set to reflect the correct process and status, improving the accuracy and clarity of notification emails.
2026-03-04 16:19:33 +01:00
59d6d25bdd Add ChangedWhen property to History entity
Added a [NotMapped] ChangedWhen property to the History entity as a wrapper for ActionDate, providing alternative get/set access without affecting the database schema.
2026-03-04 16:09:50 +01:00
9a516ab3c9 Update and align NuGet dependencies; drop net7.0 from Web
- Downgrade Microsoft.Data.SqlClient to 5.2.2 in Application and Infrastructure projects
- Downgrade Microsoft.Extensions.DependencyInjection to 8.0.1 for net8.0 in Application
- Remove net7.0 target and related packages from Web project
- Upgrade OpenApi and Caching.SqlServer to 8.0.17 (net8.0) and 9.0.6 (net9.0) in Web
- Ensures consistent, compatible package versions across solution
2026-03-04 16:09:40 +01:00
8fed342dc5 Remove [SIGNATURE_TYPE] placeholder from MailParams
The [SIGNATURE_TYPE] placeholder and its value ("signieren") were removed from the MailParams section in appsettings.Mail.json. This cleans up unused or unnecessary configuration.
2026-03-04 15:17:47 +01:00
f44643aa3e Add dynamic email placeholders for confirm/sign workflows
Refactored CreatePlaceholders to set email template values based on whether the envelope requires "read and confirm" or signature. Renamed method parameter for clarity and updated usages. Added a TODO for future method unification. Improves email content accuracy for different envelope actions.
2026-03-04 15:02:04 +01:00
86c99596c4 Implement IEnvelope interface in EnvelopeDto
EnvelopeDto now implements the IEnvelope interface, enabling it to be used wherever IEnvelope is required. No other modifications were made to the class.
2026-03-04 14:19:11 +01:00
2d0c08b2ce Implement IEnvelope and update Envelope.ReadOnly property
Envelope now implements the IEnvelope interface. The ReadOnly property is marked as [Obsolete] and delegates to the IsReadAndConfirm extension method instead of directly checking EnvelopeTypeId. Added a using directive for EnvelopeGenerator.Domain.Interfaces.
2026-03-04 14:19:02 +01:00
e0aa963184 Add IEnvelope interface and IsReadAndConfirm extension
Introduced the IEnvelope interface with an EnvelopeTypeId property in the new EnvelopeGenerator.Domain.Interfaces namespace. Added the EnvelopeExtensions static class with an IsReadAndConfirm extension method to check if EnvelopeTypeId equals 2.
2026-03-04 14:18:28 +01:00
afa3694cd7 Add MapAddedWhen to History mapping in MappingProfile
Imported necessary namespaces and updated the CreateHistoryCommand-to-History mapping to include the MapAddedWhen extension method, likely to handle automatic setting of creation timestamps or similar metadata.
2026-02-27 11:34:17 +01:00
48f4ea0c50 Use UTC for AddedWhen timestamp in CreateHistoryCommand
Changed AddedWhen to use DateTime.UtcNow instead of DateTime.Now
to ensure timestamps are stored in UTC, improving consistency
across different time zones and deployment environments.
2026-02-27 11:33:53 +01:00
74c4ddda83 Remove ModifyDocStatusCommandBase and its properties
Deleted the entire ModifyDocStatusCommandBase.cs file, including the record definition, its properties (EnvelopeId, ReceiverId, Value), and the To<TDest>() mapping method. No code or class definitions remain in this file.
2026-02-27 11:32:45 +01:00
f9b1e583df Remove SaveDocStatusCommand and always create doc status
Removed SaveDocStatusCommand and its handler, eliminating logic for updating existing document statuses. DocStatusHandler now always sends CreateDocStatusCommand with simplified parameters when handling DocSignedNotification. This change ensures a new document status is always created instead of updating existing ones.
2026-02-27 11:31:41 +01:00
7c8e0d8481 Merge branch 'refactor/api-unification' into origin/bugfix/history-inconsistency 2026-02-27 11:21:10 +01:00
6d2ec4cc0b Update DI and Logging packages to v8, adjust binding redirects
Upgraded Microsoft.Extensions.DependencyInjection.Abstractions to 8.0.2 and Microsoft.Extensions.Logging.Abstractions to 8.0.3 in project references and packages.config. Updated related binding redirects in App.config. Also modified the solution file to change the build configuration for project {83ED2617-B398-4859-8F59-B38F8807E83E} so Debug|Any CPU now maps to Release|Any CPU. Adjusted System.Memory and System.Buffers binding redirect ranges.
2026-02-23 15:54:52 +01:00
898097cdb5 Remove EnvelopeGenerator.Jobs project from solution
Deleted the EnvelopeGenerator.Jobs project and all its source files. Also removed all references to this project from the solution file, including project entries and build configurations. This cleans up the repository by eliminating unused or deprecated job-related code.
2026-02-23 10:56:03 +01:00
689a1b355a Remove EnvelopeGenerator.WorkerService project
Deleted the EnvelopeGenerator.WorkerService project and all related files, including configuration, job scheduling, and temp file management. Removed all references to the project from the solution file. This eliminates the background worker service component from the solution.
2026-02-23 10:54:32 +01:00
3b3330bd54 Enhance SQL connection security with encryption options
Added Encrypt=True and TrustServerCertificate=True to the SQL Server connection string to ensure encrypted connections and allow trusting the server certificate, improving overall connection security.
2026-02-23 10:04:34 +01:00
511fad3950 Remove try-catch from Button1/2_Click event handlers
Refactored Button1_Click and Button2_Click to eliminate local try-catch blocks, allowing exceptions to propagate. Cleaned up redundant code and improved clarity by moving variable declarations outside of the previous try blocks. Core logic remains unchanged.
2026-02-23 09:58:07 +01:00
f5f137396e Rename ChangedWhen to ActionDate; add wrapper for compatibility
Renamed the ChangedWhen property to ActionDate in the History entity, preserving the database mapping. Added a ChangedWhen property as a wrapper around ActionDate to maintain backward compatibility with existing code.
2026-02-23 09:38:13 +01:00
0d78e9b8f5 Refactor TestEnvelopeController to use MediatR
Replaced direct IEnvelopeService usage with MediatR in TestEnvelopeController. Updated GetAll to use ReadEnvelopeQuery via MediatR, removed legacy service logic, and simplified DecodeEnvelopeReceiverId. Removed unused usings, base class inheritance, and obsolete methods.
2026-02-23 09:15:50 +01:00
8258d9f43f Refactor UpdateDocStatusCommand to use generic base class
Refactored UpdateDocStatusCommand to inherit from a generic UpdateCommand base class with a new DocStatusUpdateDto record. Added explicit EnvelopeId, ReceiverId, and Value properties. Removed ChangedWhen property and implemented BuildQueryExpression for querying. Updated using directives and improved XML documentation.
2026-02-19 14:32:45 +01:00
01f3335238 Refactor ModifyDocStatusCommandBase properties
Replaced computed EnvelopeId and ReceiverId with virtual settable properties for greater flexibility. Removed Status, Receiver, and StatusChangedWhen properties. Made Value property virtual and cleaned up related code and comments.
2026-02-19 14:32:35 +01:00
1d0c758e00 Refactor CreateDocStatusCommand structure and properties
Refactored CreateDocStatusCommand to remove inheritance from ModifyDocStatusCommandBase and define its own properties. Added EnvelopeId, ReceiverId, and Value properties. Removed the AddedWhen property. The class now directly implements IRequest<DocumentStatus>.
2026-02-18 12:48:44 +01:00
711f7d12e8 Update MappingProfile for improved DocumentStatus mapping
Enhanced mapping logic in MappingProfile:
- Set Status to Created or Signed in CreateDocStatusCommand mapping based on Value property.
- Automatically set StatusChangedWhen to current UTC time in UpdateDocStatusCommand mapping.
2026-02-18 12:48:11 +01:00
43d89699a9 Add StatusChangedWhen to DocumentStatus entity
Added a nullable DateTime property, StatusChangedWhen, to the DocumentStatus entity. This property is required and mapped to the "STATUS_CHANGED_WHEN" column, enabling tracking of when the document status was last updated.
2026-02-18 12:47:23 +01:00
140 changed files with 1140 additions and 4845 deletions

View File

@@ -1,406 +0,0 @@
using EnvelopeGenerator.API.Models;
using EnvelopeGenerator.Application.Common.Extensions;
using EnvelopeGenerator.Application.Common.Interfaces.Services;
using EnvelopeGenerator.Application.EnvelopeReceivers.Queries;
using EnvelopeGenerator.Domain.Constants;
using EnvelopeGenerator.API.Extensions;
using MediatR;
using Microsoft.AspNetCore.Authentication;
using Microsoft.AspNetCore.Authentication.Cookies;
using Microsoft.AspNetCore.Mvc;
using OtpNet;
namespace EnvelopeGenerator.API.Controllers;
/// <summary>
/// REST-API für den Empfänger-Authentifizierungs-Flow.
///
/// Entspricht der Logik in EnvelopeGenerator.Web.Controllers.EnvelopeController
/// (Main + LogInEnvelope), aber gibt JSON statt Views zurück.
///
/// Der Blazor-Client (ReceiverUI) ruft diese Endpunkte auf.
///
/// FLOW:
/// 1. Client ruft GET /api/receiverauth/{key}/status → Prüft Status
/// 2. Client ruft POST /api/receiverauth/{key}/access-code → Sendet AccessCode
/// 3. Client ruft POST /api/receiverauth/{key}/tfa → Sendet TFA-Code
///
/// Nach erfolgreicher Authentifizierung wird ein Cookie gesetzt (SignInEnvelopeAsync).
/// Danach kann der Client die Dokument-Daten über die bestehenden Envelope-Endpunkte laden.
/// </summary>
[Route("api/[controller]")]
[ApiController]
public class ReceiverAuthController : ControllerBase
{
private readonly ILogger<ReceiverAuthController> _logger;
private readonly IMediator _mediator;
private readonly IEnvelopeReceiverService _envRcvService;
private readonly IEnvelopeHistoryService _historyService;
private readonly IAuthenticator _authenticator;
private readonly IReceiverService _rcvService;
private readonly IEnvelopeSmsHandler _envSmsHandler;
public ReceiverAuthController(
ILogger<ReceiverAuthController> logger,
IMediator mediator,
IEnvelopeReceiverService envRcvService,
IEnvelopeHistoryService historyService,
IAuthenticator authenticator,
IReceiverService rcvService,
IEnvelopeSmsHandler envSmsHandler)
{
_logger = logger;
_mediator = mediator;
_envRcvService = envRcvService;
_historyService = historyService;
_authenticator = authenticator;
_rcvService = rcvService;
_envSmsHandler = envSmsHandler;
}
// ══════════════════════════════════════════════════════════════
// ENDPUNKT 1: STATUS PRÜFEN
// Entspricht: Web.EnvelopeController.Main()
// ══════════════════════════════════════════════════════════════
/// <summary>
/// Prüft den aktuellen Status eines Umschlags für den Empfänger.
/// Entscheidet ob: NotFound, Rejected, Signed, AccessCode nötig, oder direkt anzeigen.
/// </summary>
/// <param name="key">Der EnvelopeReceiver-Key aus der URL (Base64-kodiert)</param>
/// <param name="cancel">Cancellation-Token</param>
/// <returns>ReceiverAuthResponse mit dem aktuellen Status</returns>
[HttpGet("{key}/status")]
public async Task<IActionResult> GetStatus([FromRoute] string key, CancellationToken cancel)
{
try
{
// ── Key dekodieren ──
if (!key.TryDecode(out var decoded))
return NotFound(new ReceiverAuthResponse { Status = "not_found" });
// ── ReadOnly-Links ──
if (decoded.GetEncodeType() == EncodeType.EnvelopeReceiverReadOnly)
{
return Ok(new ReceiverAuthResponse
{
Status = "show_document",
ReadOnly = true
});
}
// ── EnvelopeReceiver laden ──
var er = await _mediator.ReadEnvelopeReceiverAsync(key, cancel);
if (er is null)
return NotFound(new ReceiverAuthResponse { Status = "not_found" });
// ── Abgelehnt? ──
var rejRcvrs = await _historyService.ReadRejectingReceivers(er.Envelope!.Id);
if (rejRcvrs.Any())
{
await HttpContext.SignOutAsync(CookieAuthenticationDefaults.AuthenticationScheme);
return Ok(new ReceiverAuthResponse
{
Status = "rejected",
Title = er.Envelope.Title,
SenderEmail = er.Envelope.User?.Email
});
}
// ── Bereits signiert? ──
if (await _historyService.IsSigned(
envelopeId: er.Envelope.Id,
userReference: er.Receiver!.EmailAddress))
{
await HttpContext.SignOutAsync(CookieAuthenticationDefaults.AuthenticationScheme);
return Ok(new ReceiverAuthResponse
{
Status = "already_signed",
Title = er.Envelope.Title,
SenderEmail = er.Envelope.User?.Email
});
}
// ── Kein AccessCode nötig? → Direkt SignIn ──
if (!er.Envelope.UseAccessCode)
{
(string? uuid, string? signature) = decoded.ParseEnvelopeReceiverId();
var erSecretRes = await _envRcvService.ReadWithSecretByUuidSignatureAsync(
uuid: uuid!, signature: signature!);
if (erSecretRes.IsFailed)
return NotFound(new ReceiverAuthResponse { Status = "not_found" });
await HttpContext.SignInEnvelopeAsync(erSecretRes.Data, Role.ReceiverFull);
return Ok(new ReceiverAuthResponse
{
Status = "show_document",
Title = er.Envelope.Title,
Message = er.Envelope.Message,
SenderEmail = er.Envelope.User?.Email,
ReadOnly = er.Envelope.ReadOnly
});
}
// ── AccessCode nötig ──
// HINWEIS: Die E-Mail mit dem AccessCode wird NICHT hier gesendet.
// Das passiert bereits im Web-Projekt, wenn der Link generiert wird.
// Der Blazor-Flow übernimmt erst NACH dem E-Mail-Versand.
bool accessCodeAlreadyRequested = await _historyService.AccessCodeAlreadyRequested(
envelopeId: er.Envelope.Id,
userReference: er.Receiver.EmailAddress);
if (!accessCodeAlreadyRequested)
{
// AccessCode wurde noch nie angefordert — das bedeutet der Empfänger
// kommt zum ersten Mal. Wir zeichnen es auf, aber die E-Mail
// wurde bereits vom Web-Projekt gesendet.
await _historyService.RecordAsync(
er.EnvelopeId, er.Receiver.EmailAddress, EnvelopeStatus.AccessCodeRequested);
}
// ── Prüfe ob der Nutzer bereits eingeloggt ist ──
if (User.IsInRole(Role.ReceiverFull))
{
return Ok(new ReceiverAuthResponse
{
Status = "show_document",
Title = er.Envelope.Title,
Message = er.Envelope.Message,
SenderEmail = er.Envelope.User?.Email,
ReadOnly = er.Envelope.ReadOnly
});
}
return Ok(new ReceiverAuthResponse
{
Status = "requires_access_code",
Title = er.Envelope.Title,
SenderEmail = er.Envelope.User?.Email,
TfaEnabled = er.Envelope.TFAEnabled,
HasPhoneNumber = er.HasPhoneNumber
});
}
catch (Exception ex)
{
_logger.LogError(ex, "Error checking status for key {Key}", key);
return StatusCode(500, new ReceiverAuthResponse
{
Status = "error",
ErrorMessage = "Ein unerwarteter Fehler ist aufgetreten."
});
}
}
// ══════════════════════════════════════════════════════════════
// ENDPUNKT 2: ACCESS-CODE PRÜFEN
// ══════════════════════════════════════════════════════════════
/// <summary>
/// Prüft den eingegebenen Zugangscode.
/// Bei Erfolg: SignIn oder TFA-Weiterleitung.
/// Bei Fehler: Fehlermeldung zurückgeben.
/// </summary>
[HttpPost("{key}/access-code")]
public async Task<IActionResult> SubmitAccessCode(
[FromRoute] string key,
[FromBody] AccessCodeRequest request,
CancellationToken cancel)
{
try
{
// ── Key dekodieren + Daten laden ──
(string? uuid, string? signature) = key.DecodeEnvelopeReceiverId();
if (uuid is null || signature is null)
return NotFound(new ReceiverAuthResponse { Status = "not_found" });
var erSecretRes = await _envRcvService.ReadWithSecretByUuidSignatureAsync(
uuid: uuid, signature: signature);
if (erSecretRes.IsFailed)
return NotFound(new ReceiverAuthResponse { Status = "not_found" });
var erSecret = erSecretRes.Data;
// ── AccessCode prüfen ──
if (erSecret.AccessCode != request.AccessCode)
{
await _historyService.RecordAsync(
erSecret.EnvelopeId,
erSecret.Receiver!.EmailAddress,
EnvelopeStatus.AccessCodeIncorrect);
return Unauthorized(new ReceiverAuthResponse
{
Status = "requires_access_code",
Title = erSecret.Envelope!.Title,
SenderEmail = erSecret.Envelope.User?.Email,
TfaEnabled = erSecret.Envelope.TFAEnabled,
HasPhoneNumber = erSecret.HasPhoneNumber,
ErrorMessage = "Falscher Zugangscode."
});
}
// ── AccessCode korrekt ──
await _historyService.RecordAsync(
erSecret.EnvelopeId,
erSecret.Receiver!.EmailAddress,
EnvelopeStatus.AccessCodeCorrect);
// ── TFA erforderlich? ──
if (erSecret.Envelope!.TFAEnabled)
{
var rcv = erSecret.Receiver;
if (rcv.TotpSecretkey is null)
{
rcv.TotpSecretkey = _authenticator.GenerateTotpSecretKey();
await _rcvService.UpdateAsync(rcv);
}
await HttpContext.SignInEnvelopeAsync(erSecret, Role.ReceiverTFA);
if (request.PreferSms)
{
var (smsRes, expiration) = await _envSmsHandler.SendTotpAsync(erSecret);
return Ok(new ReceiverAuthResponse
{
Status = "requires_tfa",
TfaType = "sms",
TfaExpiration = expiration,
Title = erSecret.Envelope.Title,
SenderEmail = erSecret.Envelope.User?.Email,
HasPhoneNumber = erSecret.HasPhoneNumber
});
}
else
{
return Ok(new ReceiverAuthResponse
{
Status = "requires_tfa",
TfaType = "authenticator",
Title = erSecret.Envelope.Title,
SenderEmail = erSecret.Envelope.User?.Email,
HasPhoneNumber = erSecret.HasPhoneNumber
});
}
}
// ── Kein TFA → Direkt SignIn ──
await HttpContext.SignInEnvelopeAsync(erSecret, Role.ReceiverFull);
return Ok(new ReceiverAuthResponse
{
Status = "show_document",
Title = erSecret.Envelope.Title,
Message = erSecret.Envelope.Message,
SenderEmail = erSecret.Envelope.User?.Email,
ReadOnly = erSecret.Envelope.ReadOnly
});
}
catch (Exception ex)
{
_logger.LogError(ex, "Error submitting access code for key {Key}", key);
return StatusCode(500, new ReceiverAuthResponse
{
Status = "error",
ErrorMessage = "Ein unerwarteter Fehler ist aufgetreten."
});
}
}
// ══════════════════════════════════════════════════════════════
// ENDPUNKT 3: TFA-CODE PRÜFEN
// ══════════════════════════════════════════════════════════════
/// <summary>
/// Prüft den TFA-Code (SMS oder Authenticator).
/// Setzt voraus, dass der Nutzer bereits mit ReceiverTFA-Rolle eingeloggt ist.
/// </summary>
[HttpPost("{key}/tfa")]
public async Task<IActionResult> SubmitTfaCode(
[FromRoute] string key,
[FromBody] TfaCodeRequest request,
CancellationToken cancel)
{
try
{
if (!User.IsInRole(Role.ReceiverTFA))
return Unauthorized(new ReceiverAuthResponse
{
Status = "requires_access_code",
ErrorMessage = "Bitte zuerst den Zugangscode eingeben."
});
(string? uuid, string? signature) = key.DecodeEnvelopeReceiverId();
if (uuid is null || signature is null)
return NotFound(new ReceiverAuthResponse { Status = "not_found" });
var erSecretRes = await _envRcvService.ReadWithSecretByUuidSignatureAsync(
uuid: uuid, signature: signature);
if (erSecretRes.IsFailed)
return NotFound(new ReceiverAuthResponse { Status = "not_found" });
var erSecret = erSecretRes.Data;
if (erSecret.Receiver!.TotpSecretkey is null)
{
_logger.LogError("TotpSecretkey is null for receiver {Signature}", signature);
return StatusCode(500, new ReceiverAuthResponse
{
Status = "error",
ErrorMessage = "TFA-Konfiguration fehlt."
});
}
bool codeValid;
if (request.Type == "sms")
{
codeValid = _envSmsHandler.VerifyTotp(request.Code, erSecret.Receiver.TotpSecretkey);
}
else
{
codeValid = _authenticator.VerifyTotp(
request.Code,
erSecret.Receiver.TotpSecretkey,
window: VerificationWindow.RfcSpecifiedNetworkDelay);
}
if (!codeValid)
{
return Unauthorized(new ReceiverAuthResponse
{
Status = "requires_tfa",
TfaType = request.Type,
Title = erSecret.Envelope!.Title,
SenderEmail = erSecret.Envelope.User?.Email,
HasPhoneNumber = erSecret.HasPhoneNumber,
ErrorMessage = "Falscher Code."
});
}
await HttpContext.SignInEnvelopeAsync(erSecret, Role.ReceiverFull);
return Ok(new ReceiverAuthResponse
{
Status = "show_document",
Title = erSecret.Envelope!.Title,
Message = erSecret.Envelope.Message,
SenderEmail = erSecret.Envelope.User?.Email,
ReadOnly = erSecret.Envelope.ReadOnly
});
}
catch (Exception ex)
{
_logger.LogError(ex, "Error submitting TFA code for key {Key}", key);
return StatusCode(500, new ReceiverAuthResponse
{
Status = "error",
ErrorMessage = "Ein unerwarteter Fehler ist aufgetreten."
});
}
}
}

View File

@@ -1,78 +0,0 @@
namespace EnvelopeGenerator.API.Models;
/// <summary>
/// Einheitliche Antwort des ReceiverAuthControllers.
///
/// WARUM ein einziges Response-Objekt für alle Endpunkte?
/// - Der Client braucht nur ein Format zu verstehen
/// - Der Status-String bestimmt, welche Felder relevant sind
/// - Entspricht dem, was der Web-Controller bisher über ViewData verteilt hat
///
/// Status-Werte und was sie bedeuten:
/// - "requires_access_code" → AccessCode-Eingabe zeigen
/// - "requires_tfa" → TFA-Code-Eingabe zeigen (nach AccessCode)
/// - "show_document" → Dokument laden und anzeigen
/// - "already_signed" → Info-Seite "Bereits unterschrieben"
/// - "rejected" → Info-Seite "Abgelehnt"
/// - "not_found" → Fehler-Seite "Nicht gefunden"
/// - "expired" → Fehler-Seite "Link abgelaufen"
/// </summary>
public class ReceiverAuthResponse
{
/// <summary>Aktueller Status des Empfänger-Flows</summary>
public required string Status { get; init; }
/// <summary>Titel des Umschlags (z.B. "Vertragsdokument")</summary>
public string? Title { get; init; }
/// <summary>Nachricht des Absenders</summary>
public string? Message { get; init; }
/// <summary>E-Mail des Absenders (für Rückfragen-Hinweis)</summary>
public string? SenderEmail { get; init; }
/// <summary>Name des Empfängers</summary>
public string? ReceiverName { get; init; }
/// <summary>Ob TFA für diesen Umschlag aktiviert ist</summary>
public bool TfaEnabled { get; init; }
/// <summary>Ob der Empfänger eine Telefonnummer hat (für SMS-TFA)</summary>
public bool HasPhoneNumber { get; init; }
/// <summary>Ob das Dokument nur gelesen werden soll (ReadAndConfirm)</summary>
public bool ReadOnly { get; init; }
/// <summary>TFA-Typ: "sms" oder "authenticator" (wenn Status = "requires_tfa")</summary>
public string? TfaType { get; init; }
/// <summary>Ablaufzeit des SMS-Codes (für Countdown-Timer)</summary>
public DateTime? TfaExpiration { get; init; }
/// <summary>Fehlermeldung (z.B. "Falscher Zugangscode")</summary>
public string? ErrorMessage { get; init; }
}
/// <summary>
/// Request-Body für POST /api/receiverauth/{key}/access-code
/// </summary>
public class AccessCodeRequest
{
/// <summary>Der vom Empfänger eingegebene Zugangscode</summary>
public required string AccessCode { get; init; }
/// <summary>Ob SMS statt Authenticator bevorzugt wird</summary>
public bool PreferSms { get; init; }
}
/// <summary>
/// Request-Body für POST /api/receiverauth/{key}/tfa
/// </summary>
public class TfaCodeRequest
{
/// <summary>Der eingegebene TFA-Code (6-stellig)</summary>
public required string Code { get; init; }
/// <summary>"sms" oder "authenticator"</summary>
public required string Type { get; init; }
}

View File

@@ -2,6 +2,7 @@
using DigitalData.UserManager.Application.DTOs.User; using DigitalData.UserManager.Application.DTOs.User;
using EnvelopeGenerator.Domain.Constants; using EnvelopeGenerator.Domain.Constants;
using EnvelopeGenerator.Domain.Entities; using EnvelopeGenerator.Domain.Entities;
using EnvelopeGenerator.Domain.Interfaces;
using Microsoft.AspNetCore.Mvc; using Microsoft.AspNetCore.Mvc;
namespace EnvelopeGenerator.Application.Common.Dto; namespace EnvelopeGenerator.Application.Common.Dto;
@@ -10,7 +11,7 @@ namespace EnvelopeGenerator.Application.Common.Dto;
/// ///
/// </summary> /// </summary>
[ApiExplorerSettings(IgnoreApi = true)] [ApiExplorerSettings(IgnoreApi = true)]
public record EnvelopeDto public record EnvelopeDto : IEnvelope
{ {
/// <summary> /// <summary>
/// ///
@@ -74,10 +75,12 @@ public record EnvelopeDto
/// </summary> /// </summary>
public int? EnvelopeTypeId { get; set; } public int? EnvelopeTypeId { get; set; }
// TODO: use ReadAndConfirm property name
/// <summary> /// <summary>
/// ///
/// </summary> /// </summary>
public bool ReadOnly => EnvelopeTypeId == 2; [Obsolete("Use EnvelopeExtensions.IsReadAndConfirm extension metot instead.")]
public bool ReadOnly => this.IsReadAndConfirm();
/// <summary> /// <summary>
/// ///

View File

@@ -29,15 +29,12 @@ public class DocStatusHandler : INotificationHandler<DocSignedNotification>
/// <param name="notification"></param> /// <param name="notification"></param>
/// <param name="cancel"></param> /// <param name="cancel"></param>
/// <returns></returns> /// <returns></returns>
public async Task Handle(DocSignedNotification notification, CancellationToken cancel) public Task Handle(DocSignedNotification notification, CancellationToken cancel) => _sender.Send(new CreateDocStatusCommand()
{ {
await _sender.Send(new SaveDocStatusCommand() EnvelopeId = notification.EnvelopeId,
{ ReceiverId = notification.ReceiverId,
Envelope = new() { Id = notification.EnvelopeId },
Receiver = new() { Id = notification.ReceiverId},
Value = notification.PsPdfKitAnnotation is PsPdfKitAnnotation annot Value = notification.PsPdfKitAnnotation is PsPdfKitAnnotation annot
? JsonSerializer.Serialize(annot.Instant, Format.Json.ForAnnotations) ? JsonSerializer.Serialize(annot.Instant, Format.Json.ForAnnotations)
: BlankAnnotationJson : BlankAnnotationJson
}, cancel); }, cancel);
}
} }

View File

@@ -3,6 +3,7 @@ using DigitalData.EmailProfilerDispatcher.Abstraction.Entities;
using EnvelopeGenerator.Application.Common.Configurations; using EnvelopeGenerator.Application.Common.Configurations;
using EnvelopeGenerator.Domain.Entities; using EnvelopeGenerator.Domain.Entities;
using Microsoft.Extensions.Options; using Microsoft.Extensions.Options;
using EnvelopeGenerator.Domain.Interfaces;
namespace EnvelopeGenerator.Application.Common.Notifications.DocSigned.Handlers; namespace EnvelopeGenerator.Application.Common.Notifications.DocSigned.Handlers;
@@ -45,6 +46,25 @@ public class SendSignedMailHandler : SendMailHandler<DocSignedNotification>
{ "[DOCUMENT_TITLE]", notification.Envelope?.Title ?? string.Empty }, { "[DOCUMENT_TITLE]", notification.Envelope?.Title ?? string.Empty },
}; };
if (notification.Envelope.IsReadAndConfirm())
{
placeHolders["[SIGNATURE_TYPE]"] = "Lesen und bestätigen";
placeHolders["[DOCUMENT_PROCESS]"] = string.Empty;
placeHolders["[FINAL_STATUS]"] = "Lesebestätigung";
placeHolders["[FINAL_ACTION]"] = "Empfänger bestätigt";
placeHolders["[REJECTED_BY_OTHERS]"] = "anderen Empfänger abgelehnt!";
placeHolders["[RECEIVER_ACTION]"] = "bestätigt";
}
else
{
placeHolders["[SIGNATURE_TYPE]"] = "Signieren";
placeHolders["[DOCUMENT_PROCESS]"] = " und elektronisch unterschreiben";
placeHolders["[FINAL_STATUS]"] = "Signatur";
placeHolders["[FINAL_ACTION]"] = "Vertragspartner unterzeichnet";
placeHolders["[REJECTED_BY_OTHERS]"] = "anderen Vertragspartner abgelehnt! Ihre notwendige Unterzeichnung wurde verworfen.";
placeHolders["[RECEIVER_ACTION]"] = "unterschrieben";
}
return placeHolders; return placeHolders;
} }
} }

View File

@@ -8,12 +8,22 @@ namespace EnvelopeGenerator.Application.DocStatus.Commands;
/// <summary> /// <summary>
/// ///
/// </summary> /// </summary>
public record CreateDocStatusCommand : ModifyDocStatusCommandBase, IRequest<DocumentStatus> public record CreateDocStatusCommand : IRequest<DocumentStatus>
{ {
/// <summary> /// <summary>
/// Gets timestamp when this record was added. Returns the StatusChangedWhen value. ///
/// </summary> /// </summary>
public DateTime AddedWhen => StatusChangedWhen; public int EnvelopeId { get; set; }
/// <summary>
///
/// </summary>
public int ReceiverId { get; set; }
/// <summary>
/// Gets or sets the display value associated with the status.
/// </summary>
public string? Value { get; set; }
} }
/// <summary> /// <summary>

View File

@@ -1,54 +0,0 @@
using EnvelopeGenerator.Application.Common.Query;
using EnvelopeGenerator.Domain.Constants;
namespace EnvelopeGenerator.Application.DocStatus.Commands;
/// <summary>
///
/// </summary>
public record ModifyDocStatusCommandBase : EnvelopeReceiverQueryBase
{
/// <summary>
///
/// </summary>
public int? EnvelopeId => Envelope.Id;
/// <summary>
///
/// </summary>
public int? ReceiverId => Receiver.Id;
/// <summary>
///
/// </summary>
public override ReceiverQueryBase Receiver { get => base.Receiver; set => base.Receiver = value; }
/// <summary>
/// Gets the current status code.
/// </summary>
public DocumentStatus Status => Value is null ? DocumentStatus.Created : DocumentStatus.Signed;
/// <summary>
/// Gets or sets the display value associated with the status.
/// </summary>
public string? Value { get; set; }
/// <summary>
/// Gets timestamp when this record was added.
/// </summary>
public DateTime StatusChangedWhen { get; } = DateTime.Now;
/// <summary>
/// Maps the current command to a new instance of the specified type.
/// </summary>
/// <typeparam name="TDest"></typeparam>
/// <returns></returns>
public TDest To<TDest>() where TDest : ModifyDocStatusCommandBase, new()
=> new()
{
Key = Key,
Envelope = Envelope,
Receiver = Receiver,
Value = Value
};
}

View File

@@ -1,77 +0,0 @@
using DigitalData.Core.Abstraction.Application.Repository;
using EnvelopeGenerator.Domain.Entities;
using MediatR;
using Microsoft.EntityFrameworkCore;
using AutoMapper;
using EnvelopeGenerator.Application.Common.Dto;
using EnvelopeGenerator.Application.Common.Extensions;
namespace EnvelopeGenerator.Application.DocStatus.Commands;
/// <summary>
/// Represents a command to save the status of a document, either by creating a new status or updating an existing one based on the provided envelope and receiver identifiers.
/// It returns the identifier of the saved document status.
/// </summary>
public record SaveDocStatusCommand : ModifyDocStatusCommandBase, IRequest<DocumentStatusDto?>;
/// <summary>
///
/// </summary>
public class SaveDocStatusCommandHandler : IRequestHandler<SaveDocStatusCommand, DocumentStatusDto?>
{
private readonly IMapper _mapper;
private readonly IRepository<DocumentStatus> _repo;
private readonly IRepository<Envelope> _envRepo;
private readonly IRepository<Receiver> _rcvRepo;
/// <summary>
///
/// </summary>
/// <param name="mapper"></param>
/// <param name="repo"></param>
/// <param name="rcvRepo"></param>
/// <param name="envRepo"></param>
public SaveDocStatusCommandHandler(IMapper mapper, IRepository<DocumentStatus> repo, IRepository<Receiver> rcvRepo, IRepository<Envelope> envRepo)
{
_mapper = mapper;
_repo = repo;
_rcvRepo = rcvRepo;
_envRepo = envRepo;
}
/// <summary>
///
/// </summary>
/// <param name="request"></param>
/// <param name="cancel"></param>
/// <returns></returns>
public async Task<DocumentStatusDto?> Handle(SaveDocStatusCommand request, CancellationToken cancel)
{
// ceck if exists
bool isExists = await _repo.ReadOnly().Where(request).AnyAsync(cancel);
var env = await _envRepo.ReadOnly().Where(request.Envelope).FirstAsync(cancel);
var rcv = await _rcvRepo.ReadOnly().Where(request.Receiver).FirstAsync(cancel);
request.Envelope.Id = env.Id;
request.Receiver.Id = rcv.Id;
if (isExists)
{
var uReq = request.To<UpdateDocStatusCommand>();
await _repo.UpdateAsync(uReq, q => q.Where(request), cancel);
}
else
{
var cReq = request.To<CreateDocStatusCommand>();
await _repo.CreateAsync(cReq, cancel);
}
var docStatus = await _repo.ReadOnly().Where(request).SingleOrDefaultAsync(cancel);
return _mapper.Map<DocumentStatusDto>(docStatus);
}
}

View File

@@ -1,14 +1,41 @@
using EnvelopeGenerator.Domain; using EnvelopeGenerator.Application.Common.Commands;
using EnvelopeGenerator.Domain.Entities;
using System.Linq.Expressions;
namespace EnvelopeGenerator.Application.DocStatus.Commands; namespace EnvelopeGenerator.Application.DocStatus.Commands;
/// <summary> /// <summary>
/// ///
/// </summary> /// </summary>
public record UpdateDocStatusCommand : ModifyDocStatusCommandBase /// <param name="Value"></param>
public record DocStatusUpdateDto(string? Value);
/// <summary>
///
/// </summary>
public record UpdateDocStatusCommand : UpdateCommand<DocStatusUpdateDto, DocumentStatus>
{ {
/// <summary> /// <summary>
/// Gets timestamp when this record was added. Returns the StatusChangedWhen value. ///
/// </summary> /// </summary>
public DateTime? ChangedWhen => StatusChangedWhen; public int EnvelopeId { get; set; }
/// <summary>
///
/// </summary>
public int ReceiverId { get; set; }
/// <summary>
/// Gets or sets the display value associated with the status.
/// </summary>
public string? Value { get; set; }
/// <summary>
///
/// </summary>
/// <returns></returns>
public override Expression<Func<DocumentStatus, bool>> BuildQueryExpression()
{
return ds => ds.EnvelopeId == EnvelopeId && ds.ReceiverId == ReceiverId;
}
} }

View File

@@ -18,11 +18,16 @@ public class MappingProfile : Profile
CreateMap<CreateDocStatusCommand, DocumentStatus>() CreateMap<CreateDocStatusCommand, DocumentStatus>()
.ForMember(dest => dest.Envelope, opt => opt.Ignore()) .ForMember(dest => dest.Envelope, opt => opt.Ignore())
.ForMember(dest => dest.Receiver, opt => opt.Ignore()) .ForMember(dest => dest.Receiver, opt => opt.Ignore())
.ForMember(dest => dest.Status, opt => opt.MapFrom(
src => src.Value == null
? Domain.Constants.DocumentStatus.Created
: Domain.Constants.DocumentStatus.Signed))
.MapAddedWhen(); .MapAddedWhen();
CreateMap<UpdateDocStatusCommand, DocumentStatus>() CreateMap<UpdateDocStatusCommand, DocumentStatus>()
.ForMember(dest => dest.Envelope, opt => opt.Ignore()) .ForMember(dest => dest.Envelope, opt => opt.Ignore())
.ForMember(dest => dest.Receiver, opt => opt.Ignore()) .ForMember(dest => dest.Receiver, opt => opt.Ignore())
.ForMember(dest => dest.StatusChangedWhen, opt => opt.MapFrom(src => DateTime.UtcNow))
.MapChangedWhen(); .MapChangedWhen();
} }
} }

View File

@@ -37,7 +37,7 @@
</ItemGroup> </ItemGroup>
<ItemGroup Condition="'$(TargetFramework)' == 'net8.0'"> <ItemGroup Condition="'$(TargetFramework)' == 'net8.0'">
<PackageReference Include="Microsoft.Extensions.DependencyInjection" Version="9.0.5" /> <PackageReference Include="Microsoft.Extensions.DependencyInjection" Version="8.0.1" />
</ItemGroup> </ItemGroup>
<ItemGroup Condition="'$(TargetFramework)' == 'net9.0'"> <ItemGroup Condition="'$(TargetFramework)' == 'net9.0'">
@@ -80,7 +80,7 @@
<ItemGroup Condition="'$(TargetFramework)' == 'net7.0'"> <ItemGroup Condition="'$(TargetFramework)' == 'net7.0'">
<PackageReference Include="AutoMapper" Version="13.0.1" /> <PackageReference Include="AutoMapper" Version="13.0.1" />
<PackageReference Include="Microsoft.Data.SqlClient" Version="6.1.4" /> <PackageReference Include="Microsoft.Data.SqlClient" Version="5.2.2" />
<PackageReference Include="CommandDotNet"> <PackageReference Include="CommandDotNet">
<Version>7.0.5</Version> <Version>7.0.5</Version>
</PackageReference> </PackageReference>
@@ -88,7 +88,6 @@
<ItemGroup Condition="'$(TargetFramework)' == 'net8.0'"> <ItemGroup Condition="'$(TargetFramework)' == 'net8.0'">
<PackageReference Include="AutoMapper" Version="14.0.0" /> <PackageReference Include="AutoMapper" Version="14.0.0" />
<PackageReference Include="Microsoft.Data.SqlClient" Version="6.0.2" />
<PackageReference Include="CommandDotNet"> <PackageReference Include="CommandDotNet">
<Version>8.1.1</Version> <Version>8.1.1</Version>
</PackageReference> </PackageReference>
@@ -96,7 +95,6 @@
<ItemGroup Condition="'$(TargetFramework)' == 'net9.0'"> <ItemGroup Condition="'$(TargetFramework)' == 'net9.0'">
<PackageReference Include="AutoMapper" Version="14.0.0" /> <PackageReference Include="AutoMapper" Version="14.0.0" />
<PackageReference Include="Microsoft.Data.SqlClient" Version="6.0.2" />
<PackageReference Include="CommandDotNet"> <PackageReference Include="CommandDotNet">
<Version>8.1.1</Version> <Version>8.1.1</Version>
</PackageReference> </PackageReference>

View File

@@ -34,7 +34,7 @@ public record CreateHistoryCommand : EnvelopeReceiverQueryBase, IRequest<History
/// <summary> /// <summary>
/// ///
/// </summary> /// </summary>
public DateTime AddedWhen { get; } = DateTime.Now; public DateTime AddedWhen { get; } = DateTime.UtcNow;
/// <summary> /// <summary>
/// ///

View File

@@ -1,4 +1,5 @@
using AutoMapper; using AutoMapper;
using EnvelopeGenerator.Application.Common.Extensions;
using EnvelopeGenerator.Application.Histories.Commands; using EnvelopeGenerator.Application.Histories.Commands;
using EnvelopeGenerator.Domain.Entities; using EnvelopeGenerator.Domain.Entities;
@@ -17,6 +18,7 @@ public class MappingProfile: Profile
CreateMap<CreateHistoryCommand, History>() CreateMap<CreateHistoryCommand, History>()
.ForMember(dest => dest.Envelope, opt => opt.Ignore()) .ForMember(dest => dest.Envelope, opt => opt.Ignore())
.ForMember(dest => dest.Sender, opt => opt.Ignore()) .ForMember(dest => dest.Sender, opt => opt.Ignore())
.ForMember(dest => dest.Receiver, opt => opt.Ignore()); .ForMember(dest => dest.Receiver, opt => opt.Ignore())
.MapAddedWhen();
} }
} }

View File

@@ -190,6 +190,13 @@ public static class Extensions
/// <returns></returns> /// <returns></returns>
public static string SignDoc(this IStringLocalizer localizer) => localizer[nameof(SignDoc)].Value; public static string SignDoc(this IStringLocalizer localizer) => localizer[nameof(SignDoc)].Value;
/// <summary>
///
/// </summary>
/// <param name="localizer"></param>
/// <returns></returns>
public static string ConfirmDoc(this IStringLocalizer localizer) => localizer[nameof(ConfirmDoc)].Value;
/// <summary> /// <summary>
/// ///
/// </summary> /// </summary>
@@ -204,6 +211,13 @@ public static class Extensions
/// <returns></returns> /// <returns></returns>
public static string DocSigned(this IStringLocalizer localizer) => localizer[nameof(DocSigned)].Value; public static string DocSigned(this IStringLocalizer localizer) => localizer[nameof(DocSigned)].Value;
/// <summary>
///
/// </summary>
/// <param name="localizer"></param>
/// <returns></returns>
public static string DocConfirmed(this IStringLocalizer localizer) => localizer[nameof(DocConfirmed)].Value;
/// <summary> /// <summary>
/// ///
/// </summary> /// </summary>
@@ -239,6 +253,13 @@ public static class Extensions
/// <returns></returns> /// <returns></returns>
public static string SigAgree(this IStringLocalizer localizer) => localizer[nameof(SigAgree)].Value; public static string SigAgree(this IStringLocalizer localizer) => localizer[nameof(SigAgree)].Value;
/// <summary>
///
/// </summary>
/// <param name="localizer"></param>
/// <returns></returns>
public static string ConfirmAgree(this IStringLocalizer localizer) => localizer[nameof(ConfirmAgree)].Value;
/// <summary> /// <summary>
/// ///
/// </summary> /// </summary>
@@ -267,6 +288,13 @@ public static class Extensions
/// <returns></returns> /// <returns></returns>
public static string RejectionInfo1(this IStringLocalizer localizer) => localizer[nameof(RejectionInfo1)].Value; public static string RejectionInfo1(this IStringLocalizer localizer) => localizer[nameof(RejectionInfo1)].Value;
/// <summary>
///
/// </summary>
/// <param name="localizer"></param>
/// <returns></returns>
public static string RejectionInfo1ForConfirmation(this IStringLocalizer localizer) => localizer[nameof(RejectionInfo1ForConfirmation)].Value;
/// <summary> /// <summary>
/// ///
/// </summary> /// </summary>
@@ -295,6 +323,13 @@ public static class Extensions
/// <returns></returns> /// <returns></returns>
public static string SigningProcessTitle(this IStringLocalizer localizer) => localizer[nameof(SigningProcessTitle)].Value; public static string SigningProcessTitle(this IStringLocalizer localizer) => localizer[nameof(SigningProcessTitle)].Value;
/// <summary>
///
/// </summary>
/// <param name="localizer"></param>
/// <returns></returns>
public static string ConfirmationProcessTitle(this IStringLocalizer localizer) => localizer[nameof(ConfirmationProcessTitle)].Value;
/// <summary> /// <summary>
/// ///
/// </summary> /// </summary>

View File

@@ -250,7 +250,7 @@
<value>Sie können bei Bedarf mit {0}, &lt;a href="mailto:{1}?subject={2}&amp;body=Sehr geehrte(r)%20{0},%0A%0A%0A"&gt;{1}&lt;/a&gt; Kontakt aufnehmen.</value> <value>Sie können bei Bedarf mit {0}, &lt;a href="mailto:{1}?subject={2}&amp;body=Sehr geehrte(r)%20{0},%0A%0A%0A"&gt;{1}&lt;/a&gt; Kontakt aufnehmen.</value>
</data> </data>
<data name="RejectionInfo2_ext" xml:space="preserve"> <data name="RejectionInfo2_ext" xml:space="preserve">
<value>Das Vorgang wurde von einer der beteiligten Parteien abgelehnt. Sie können bei Bedarf mit {0}, &lt;a href="mailto:{1}?subject={2}&amp;body=Sehr geehrte(r)%20{0},%0A%0A%0A"&gt;{1}&lt;/a&gt; Kontakt aufnehmen.</value> <value>Der Vorgang wurde von einer der beteiligten Parteien abgelehnt. Sie können bei Bedarf mit {0}, &lt;a href="mailto:{1}?subject={2}&amp;body=Sehr geehrte(r)%20{0},%0A%0A%0A"&gt;{1}&lt;/a&gt; Kontakt aufnehmen.</value>
</data> </data>
<data name="RejectionReasonQ" xml:space="preserve"> <data name="RejectionReasonQ" xml:space="preserve">
<value>Bitte geben Sie einen Grund an:</value> <value>Bitte geben Sie einen Grund an:</value>
@@ -447,4 +447,34 @@
<data name="DocumentReset" xml:space="preserve"> <data name="DocumentReset" xml:space="preserve">
<value>Dokument wurde zurückgesetzt.</value> <value>Dokument wurde zurückgesetzt.</value>
</data> </data>
<data name="DocumentSuccessfullyConfirmed" xml:space="preserve">
<value>Dokument erfolgreich gelesen und bestätigt!</value>
</data>
<data name="DocumentConfirmedConfirmationMessage" xml:space="preserve">
<value>Sie haben das Dokument gelesen und bestätigt. Im Anschluss erhalten Sie eine schriftliche Bestätigung.</value>
</data>
<data name="Confirm" xml:space="preserve">
<value>Bestätigen</value>
</data>
<data name="RejectionInfo1Confirmation" xml:space="preserve">
<value>Dieser Bestätigungsvorgang wurde abgelehnt!</value>
</data>
<data name="ConfirmDoc" xml:space="preserve">
<value>Dokument bestätigen</value>
</data>
<data name="DocConfirmed" xml:space="preserve">
<value>Dokument bestätigt</value>
</data>
<data name="ConfirmAgree" xml:space="preserve">
<value>Durch Klick auf Abschließen bestätige ich, das Dokument gelesen und zur Kenntnis genommen zu haben.</value>
</data>
<data name="ConfirmedBy" xml:space="preserve">
<value>Bestätigt von</value>
</data>
<data name="ConfirmationProcessTitle" xml:space="preserve">
<value>Titel des Lesebetätigungs-Vorgangs</value>
</data>
<data name="Confirmations" xml:space="preserve">
<value>Bestätigungen</value>
</data>
</root> </root>

View File

@@ -447,4 +447,34 @@
<data name="DocumentReset" xml:space="preserve"> <data name="DocumentReset" xml:space="preserve">
<value>Document has been reset.</value> <value>Document has been reset.</value>
</data> </data>
<data name="DocumentSuccessfullyConfirmed" xml:space="preserve">
<value>Document successfully red and confirmed!</value>
</data>
<data name="DocumentConfirmedConfirmationMessage" xml:space="preserve">
<value>You have read and confirmed the document. You will receive a written confirmation afterwards.</value>
</data>
<data name="Confirm" xml:space="preserve">
<value>Confirm</value>
</data>
<data name="RejectionInfo1Confirmation" xml:space="preserve">
<value>This confirmation process has been rejected!</value>
</data>
<data name="ConfirmDoc" xml:space="preserve">
<value>Confirm Document</value>
</data>
<data name="DocConfirmed" xml:space="preserve">
<value>Document confirmed</value>
</data>
<data name="ConfirmAgree" xml:space="preserve">
<value>By clicking on “Complete”, I confirm that I have read and taken note of the document.</value>
</data>
<data name="ConfirmedBy" xml:space="preserve">
<value>Confirmed by</value>
</data>
<data name="ConfirmationProcessTitle" xml:space="preserve">
<value>Title of the read confirmation process</value>
</data>
<data name="Confirmations" xml:space="preserve">
<value>Confirmations</value>
</data>
</root> </root>

View File

@@ -447,4 +447,34 @@
<data name="DocumentReset" xml:space="preserve"> <data name="DocumentReset" xml:space="preserve">
<value>Le document a été réinitialisé.</value> <value>Le document a été réinitialisé.</value>
</data> </data>
<data name="DocumentSuccessfullyConfirmed" xml:space="preserve">
<value>Document lu et confirmé avec succès !</value>
</data>
<data name="DocumentConfirmedConfirmationMessage" xml:space="preserve">
<value>Vous avez lu et confirmé le document. Vous recevrez une confirmation écrite par la suite.</value>
</data>
<data name="Confirm" xml:space="preserve">
<value>Confirmer</value>
</data>
<data name="RejectionInfo1Confirmation" xml:space="preserve">
<value>Cette procédure de confirmation a été rejetée !</value>
</data>
<data name="ConfirmDoc" xml:space="preserve">
<value>Confirmer le document</value>
</data>
<data name="DocConfirmed" xml:space="preserve">
<value>Document confirmé</value>
</data>
<data name="ConfirmAgree" xml:space="preserve">
<value>En cliquant sur « Terminer », je confirme avoir lu et pris connaissance du document.</value>
</data>
<data name="ConfirmedBy" xml:space="preserve">
<value>Confirmé par</value>
</data>
<data name="ConfirmationProcessTitle" xml:space="preserve">
<value>Titre de la procédure de confirmation de lecture</value>
</data>
<data name="Confirmations" xml:space="preserve">
<value>Confirmations</value>
</data>
</root> </root>

View File

@@ -12,6 +12,7 @@ using EnvelopeGenerator.Application.Common.Dto.EnvelopeReceiverReadOnly;
using EnvelopeGenerator.Application.Common.Extensions; using EnvelopeGenerator.Application.Common.Extensions;
using EnvelopeGenerator.Application.Common.Interfaces.Services; using EnvelopeGenerator.Application.Common.Interfaces.Services;
using MediatR; using MediatR;
using EnvelopeGenerator.Domain.Interfaces;
namespace EnvelopeGenerator.Application.Services; namespace EnvelopeGenerator.Application.Services;
@@ -49,14 +50,33 @@ public class EnvelopeMailService : EmailOutService, IEnvelopeMailService
_sender = sender; _sender = sender;
} }
private async Task<Dictionary<string, string>> CreatePlaceholders(string? accessCode = null, EnvelopeReceiverDto? envelopeReceiverDto = null) private async Task<Dictionary<string, string>> CreatePlaceholders(string? accessCode = null, EnvelopeReceiverDto? er = null)
{ {
if (er!.Envelope.IsReadAndConfirm())
{
_placeholders["[SIGNATURE_TYPE]"] = "Lesen und bestätigen";
_placeholders["[DOCUMENT_PROCESS]"] = string.Empty;
_placeholders["[FINAL_STATUS]"] = "Lesebestätigung";
_placeholders["[FINAL_ACTION]"] = "Empfänger bestätigt";
_placeholders["[REJECTED_BY_OTHERS]"] = "anderen Empfänger abgelehnt!";
_placeholders["[RECEIVER_ACTION]"] = "bestätigt";
}
else
{
_placeholders["[SIGNATURE_TYPE]"] = "Signieren";
_placeholders["[DOCUMENT_PROCESS]"] = " und elektronisch unterschreiben";
_placeholders["[FINAL_STATUS]"] = "Signatur";
_placeholders["[FINAL_ACTION]"] = "Vertragspartner unterzeichnet";
_placeholders["[REJECTED_BY_OTHERS]"] = "anderen Vertragspartner abgelehnt! Ihre notwendige Unterzeichnung wurde verworfen.";
_placeholders["[RECEIVER_ACTION]"] = "unterschrieben";
}
if (accessCode is not null) if (accessCode is not null)
_placeholders["[DOCUMENT_ACCESS_CODE]"] = accessCode; _placeholders["[DOCUMENT_ACCESS_CODE]"] = accessCode;
if (envelopeReceiverDto?.Envelope is not null && envelopeReceiverDto.Receiver is not null) if (er?.Envelope is not null && er.Receiver is not null)
{ {
var erId = (envelopeReceiverDto.Envelope.Uuid, envelopeReceiverDto.Receiver.Signature).ToEnvelopeKey(); var erId = (er.Envelope.Uuid, er.Receiver.Signature).ToEnvelopeKey();
var sigHost = await _configService.ReadDefaultSignatureHost(); var sigHost = await _configService.ReadDefaultSignatureHost();
var linkToDoc = $"{sigHost}/EnvelopeKey/{erId}"; var linkToDoc = $"{sigHost}/EnvelopeKey/{erId}";
_placeholders["[LINK_TO_DOCUMENT]"] = linkToDoc; _placeholders["[LINK_TO_DOCUMENT]"] = linkToDoc;
@@ -66,6 +86,7 @@ public class EnvelopeMailService : EmailOutService, IEnvelopeMailService
return _placeholders; return _placeholders;
} }
// TODO: merge the two CreatePlaceholders methods by using a common parameter object containing all the required information to create the place holders.
private async Task<Dictionary<string, string>> CreatePlaceholders(EnvelopeReceiverReadOnlyDto? readOnlyDto = null) private async Task<Dictionary<string, string>> CreatePlaceholders(EnvelopeReceiverReadOnlyDto? readOnlyDto = null)
{ {
if (readOnlyDto?.Envelope is not null && readOnlyDto.Receiver is not null) if (readOnlyDto?.Envelope is not null && readOnlyDto.Receiver is not null)
@@ -124,7 +145,7 @@ public class EnvelopeMailService : EmailOutService, IEnvelopeMailService
return acResult.ToFail<int>().Notice(LogLevel.Error, "Therefore, access code cannot be sent"); return acResult.ToFail<int>().Notice(LogLevel.Error, "Therefore, access code cannot be sent");
var accessCode = acResult.Data; var accessCode = acResult.Data;
var placeholders = await CreatePlaceholders(accessCode: accessCode, envelopeReceiverDto: dto); var placeholders = await CreatePlaceholders(accessCode: accessCode, er: dto);
// Add optional place holders. // Add optional place holders.
if (optionalPlaceholders is not null) if (optionalPlaceholders is not null)

View File

@@ -487,10 +487,6 @@
<Project>{6EA0C51F-C2B1-4462-8198-3DE0B32B74F8}</Project> <Project>{6EA0C51F-C2B1-4462-8198-3DE0B32B74F8}</Project>
<Name>EnvelopeGenerator.CommonServices</Name> <Name>EnvelopeGenerator.CommonServices</Name>
</ProjectReference> </ProjectReference>
<ProjectReference Include="..\EnvelopeGenerator.CommonServices\EnvelopeGenerator.CommonServices.vbproj">
<Project>{6ea0c51f-c2b1-4462-8198-3de0b32b74f8}</Project>
<Name>EnvelopeGenerator.CommonServices</Name>
</ProjectReference>
<ProjectReference Include="..\EnvelopeGenerator.Domain\EnvelopeGenerator.Domain.csproj"> <ProjectReference Include="..\EnvelopeGenerator.Domain\EnvelopeGenerator.Domain.csproj">
<Project>{4f32a98d-e6f0-4a09-bd97-1cf26107e837}</Project> <Project>{4f32a98d-e6f0-4a09-bd97-1cf26107e837}</Project>
<Name>EnvelopeGenerator.Domain</Name> <Name>EnvelopeGenerator.Domain</Name>

View File

@@ -27,6 +27,7 @@ Partial Class frmFinalizePDF
Me.Label2 = New System.Windows.Forms.Label() Me.Label2 = New System.Windows.Forms.Label()
Me.Button1 = New System.Windows.Forms.Button() Me.Button1 = New System.Windows.Forms.Button()
Me.Button2 = New System.Windows.Forms.Button() Me.Button2 = New System.Windows.Forms.Button()
Me.Button3 = New System.Windows.Forms.Button()
Me.txtResult = New System.Windows.Forms.TextBox() Me.txtResult = New System.Windows.Forms.TextBox()
Me.txtEnvelope = New System.Windows.Forms.TextBox() Me.txtEnvelope = New System.Windows.Forms.TextBox()
Me.SuspendLayout() Me.SuspendLayout()
@@ -75,6 +76,15 @@ Partial Class frmFinalizePDF
Me.Button2.Text = "Merge Json" Me.Button2.Text = "Merge Json"
Me.Button2.UseVisualStyleBackColor = True Me.Button2.UseVisualStyleBackColor = True
' '
'Button3
'
Me.Button3.Location = New System.Drawing.Point(15, 160)
Me.Button3.Name = "Button3"
Me.Button3.Size = New System.Drawing.Size(166, 23)
Me.Button3.TabIndex = 5
Me.Button3.Text = "Full Finalize Test"
Me.Button3.UseVisualStyleBackColor = True
'
'txtResult 'txtResult
' '
Me.txtResult.Location = New System.Drawing.Point(333, 12) Me.txtResult.Location = New System.Drawing.Point(333, 12)
@@ -97,6 +107,7 @@ Partial Class frmFinalizePDF
Me.AutoScaleMode = System.Windows.Forms.AutoScaleMode.Font Me.AutoScaleMode = System.Windows.Forms.AutoScaleMode.Font
Me.ClientSize = New System.Drawing.Size(800, 450) Me.ClientSize = New System.Drawing.Size(800, 450)
Me.Controls.Add(Me.txtResult) Me.Controls.Add(Me.txtResult)
Me.Controls.Add(Me.Button3)
Me.Controls.Add(Me.Button2) Me.Controls.Add(Me.Button2)
Me.Controls.Add(Me.Button1) Me.Controls.Add(Me.Button1)
Me.Controls.Add(Me.Label2) Me.Controls.Add(Me.Label2)
@@ -116,5 +127,6 @@ Partial Class frmFinalizePDF
Friend WithEvents Label2 As Label Friend WithEvents Label2 As Label
Friend WithEvents Button1 As Button Friend WithEvents Button1 As Button
Friend WithEvents Button2 As Button Friend WithEvents Button2 As Button
Friend WithEvents Button3 As Button
Friend WithEvents txtResult As TextBox Friend WithEvents txtResult As TextBox
End Class End Class

View File

@@ -7,11 +7,12 @@ Imports GdPicture14
Imports Newtonsoft.Json.Linq Imports Newtonsoft.Json.Linq
Imports EnvelopeGenerator.Infrastructure Imports EnvelopeGenerator.Infrastructure
Imports Microsoft.EntityFrameworkCore Imports Microsoft.EntityFrameworkCore
Imports System.Text
Imports DigitalData.Core.Abstractions Imports DigitalData.Core.Abstractions
Imports DigitalData.Core.Abstraction.Application.Repository
Imports EnvelopeGenerator.Domain.Entities
Public Class frmFinalizePDF Public Class frmFinalizePDF
Private Const CONNECTIONSTRING = "Server=sDD-VMP04-SQL17\DD_DEVELOP01;Database=DD_ECM;User Id=sa;Password=+bk8oAbbQP1AzoHtvZUbd+Mbok2f8Fl4miEx1qssJ5yEaEWoQJ9prg4L14fURpPnqi1WMNs9fE4=;" Private Const CONNECTIONSTRING = "Server=sDD-VMP04-SQL17\DD_DEVELOP01;Database=DD_ECM;User Id=sa;Password=+bk8oAbbQP1AzoHtvZUbd+Mbok2f8Fl4miEx1qssJ5yEaEWoQJ9prg4L14fURpPnqi1WMNs9fE4=;" + "Encrypt=True;TrustServerCertificate=True;"
Private Database As MSSQLServer Private Database As MSSQLServer
Private LogConfig As LogConfig Private LogConfig As LogConfig
@@ -93,8 +94,6 @@ Public Class frmFinalizePDF
End Function End Function
Private Sub Button1_Click(sender As Object, e As EventArgs) Handles Button1.Click Private Sub Button1_Click(sender As Object, e As EventArgs) Handles Button1.Click
Try
Dim oTable = LoadAnnotationDataForEnvelope() Dim oTable = LoadAnnotationDataForEnvelope()
Dim oJsonList = oTable.Rows. Dim oJsonList = oTable.Rows.
Cast(Of DataRow). Cast(Of DataRow).
@@ -110,22 +109,9 @@ Public Class frmFinalizePDF
File.WriteAllBytes(oNewPath, oNewBuffer) File.WriteAllBytes(oNewPath, oNewBuffer)
Process.Start(oNewPath) Process.Start(oNewPath)
Catch ex As Exception
Dim exMsg As StringBuilder = New StringBuilder(ex.Message).AppendLine()
Dim innerEx = ex.InnerException
While (innerEx IsNot Nothing)
exMsg.AppendLine(innerEx.Message)
innerEx = innerEx.InnerException
End While
MsgBox(exMsg.ToString(), MsgBoxStyle.Critical)
End Try
End Sub End Sub
Private Sub Button2_Click(sender As Object, e As EventArgs) Handles Button2.Click Private Sub Button2_Click(sender As Object, e As EventArgs) Handles Button2.Click
Try
Dim oTable = LoadAnnotationDataForEnvelope() Dim oTable = LoadAnnotationDataForEnvelope()
Dim oJsonList = oTable.Rows. Dim oJsonList = oTable.Rows.
Cast(Of DataRow). Cast(Of DataRow).
@@ -139,10 +125,87 @@ Public Class frmFinalizePDF
oJObject1.Merge(oJObject2) oJObject1.Merge(oJObject2)
txtResult.Text = oJObject1.ToString() txtResult.Text = oJObject1.ToString()
End Sub
Private Sub Button3_Click(sender As Object, e As EventArgs) Handles Button3.Click
Try
Dim envelopeId As Integer = CInt(txtEnvelope.Text)
Dim log As New System.Text.StringBuilder()
' 1. Load annotation JSON data (same as Service)
Dim oTable = LoadAnnotationDataForEnvelope()
Dim oJsonList = oTable.Rows.
Cast(Of DataRow).
Select(Function(r As DataRow) r.Item("VALUE").ToString()).
ToList()
log.AppendLine($"Annotation JSON count: {oJsonList.Count}")
' 2. Load document bytes (same as Service)
Dim oBuffer As Byte() = ReadEnvelope(envelopeId)
log.AppendLine($"Document bytes: {oBuffer.Length}")
' 3. Check what BurnAnnotsToPDF will do internally
Using scope = Factory.Shared.ScopeFactory.CreateScope()
Dim envRepo = scope.ServiceProvider.Repository(Of Envelope)()
Dim envelope = envRepo.Where(Function(env) env.Id = envelopeId).FirstOrDefault()
If envelope Is Nothing Then
log.AppendLine("ERROR: Envelope not found in EF Core!")
txtResult.Text = log.ToString()
Return
End If
log.AppendLine($"Envelope found: Id={envelope.Id}, EnvelopeTypeId={envelope.EnvelopeTypeId}")
log.AppendLine($"ReadOnly (IsReadAndConfirm): {envelope.ReadOnly}")
If envelope.ReadOnly Then
log.AppendLine(">>> EARLY RETURN: ReadOnly=True, original PDF returned without burning")
txtResult.Text = log.ToString()
Return
End If
Dim sigRepo = scope.ServiceProvider.Repository(Of Signature)()
Dim elements = sigRepo _
.Where(Function(sig) sig.Document.EnvelopeId = envelopeId) _
.Include(Function(sig) sig.Annotations) _
.ToList()
log.AppendLine($"Elements (Signature) count: {elements.Count}")
If elements.Any() Then
log.AppendLine(">>> PATH: BurnElementAnnotsToPDF (new element-based path)")
For Each elem In elements
Dim annotCount = If(elem.Annotations IsNot Nothing, elem.Annotations.Count(), 0)
log.AppendLine($" Element Id={elem.Id}, Page={elem.Page}, X={elem.X}, Y={elem.Y}, W={elem.Width}, H={elem.Height}, Annotations={annotCount}")
If elem.Annotations IsNot Nothing Then
For Each annot In elem.Annotations
log.AppendLine($" Annot: Name={annot.Name}, Type={annot.Type}, X={annot.X}, Y={annot.Y}, W={annot.Width}, H={annot.Height}")
Next
End If
Next
Else
log.AppendLine(">>> PATH: BurnInstantJSONAnnotsToPDF (old JSON-based path)")
End If
End Using
' 4. Actually call BurnAnnotsToPDF (same as Service)
log.AppendLine("")
log.AppendLine("Calling BurnAnnotsToPDF...")
Dim oNewBuffer = PDFBurner.BurnAnnotsToPDF(oBuffer, oJsonList, envelopeId)
log.AppendLine($"Result bytes: {oNewBuffer.Length}")
log.AppendLine($"Same as input: {oBuffer.Length = oNewBuffer.Length AndAlso oBuffer.SequenceEqual(oNewBuffer)}")
' 5. Write output
Dim desktopPath As String = Environment.GetFolderPath(Environment.SpecialFolder.Desktop)
Dim oNewPath = Path.Combine(desktopPath, $"E{txtEnvelope.Text}_FullTest.burned.pdf")
File.WriteAllBytes(oNewPath, oNewBuffer)
log.AppendLine($"Output: {oNewPath}")
txtResult.Text = log.ToString()
Process.Start(oNewPath)
Catch ex As Exception Catch ex As Exception
MsgBox(ex.Message, MsgBoxStyle.Critical) txtResult.Text = $"ERROR: {ex.Message}{vbCrLf}{vbCrLf}{ex.ToString()}"
End Try End Try
End Sub End Sub
End Class End Class

View File

@@ -15,13 +15,13 @@
<package id="DigitalData.Modules.Messaging" version="1.9.8" targetFramework="net462" /> <package id="DigitalData.Modules.Messaging" version="1.9.8" targetFramework="net462" />
<package id="DocumentFormat.OpenXml" version="3.2.0" targetFramework="net462" /> <package id="DocumentFormat.OpenXml" version="3.2.0" targetFramework="net462" />
<package id="DocumentFormat.OpenXml.Framework" version="3.2.0" targetFramework="net462" /> <package id="DocumentFormat.OpenXml.Framework" version="3.2.0" targetFramework="net462" />
<package id="EntityFramework" version="6.5.1" targetFramework="net462" /> <package id="EntityFramework" version="6.4.4" targetFramework="net462" />
<package id="EntityFramework.Firebird" version="6.4.0" targetFramework="net462" /> <package id="EntityFramework.Firebird" version="6.4.0" targetFramework="net462" />
<package id="FirebirdSql.Data.FirebirdClient" version="7.5.0" targetFramework="net462" /> <package id="FirebirdSql.Data.FirebirdClient" version="7.5.0" targetFramework="net462" />
<package id="GdPicture" version="14.3.3" targetFramework="net462" /> <package id="GdPicture" version="14.3.3" targetFramework="net462" />
<package id="GdPicture.runtimes.windows" version="14.3.3" targetFramework="net462" /> <package id="GdPicture.runtimes.windows" version="14.3.3" targetFramework="net462" />
<package id="Microsoft.AspNet.WebApi.Client" version="6.0.0" targetFramework="net462" /> <package id="Microsoft.AspNet.WebApi.Client" version="6.0.0" targetFramework="net462" />
<package id="Microsoft.Bcl.AsyncInterfaces" version="9.0.0" targetFramework="net462" /> <package id="Microsoft.Bcl.AsyncInterfaces" version="8.0.0" targetFramework="net462" />
<package id="Microsoft.Bcl.Cryptography" version="9.0.0" targetFramework="net462" /> <package id="Microsoft.Bcl.Cryptography" version="9.0.0" targetFramework="net462" />
<package id="Microsoft.Bcl.HashCode" version="1.1.1" targetFramework="net462" /> <package id="Microsoft.Bcl.HashCode" version="1.1.1" targetFramework="net462" />
<package id="Microsoft.CSharp" version="4.7.0" targetFramework="net462" /> <package id="Microsoft.CSharp" version="4.7.0" targetFramework="net462" />
@@ -62,10 +62,10 @@
<package id="protobuf-net.Core" version="3.2.46" targetFramework="net462" /> <package id="protobuf-net.Core" version="3.2.46" targetFramework="net462" />
<package id="RtfPipe" version="2.0.7677.4303" targetFramework="net462" /> <package id="RtfPipe" version="2.0.7677.4303" targetFramework="net462" />
<package id="S22.Imap" version="3.6.0.0" targetFramework="net462" /> <package id="S22.Imap" version="3.6.0.0" targetFramework="net462" />
<package id="System.Buffers" version="4.6.1" targetFramework="net462" /> <package id="System.Buffers" version="4.6.0" targetFramework="net462" />
<package id="System.ClientModel" version="1.8.0" targetFramework="net462" /> <package id="System.ClientModel" version="1.8.0" targetFramework="net462" />
<package id="System.CodeDom" version="9.0.0" targetFramework="net462" /> <package id="System.CodeDom" version="8.0.0" targetFramework="net462" />
<package id="System.Collections.Immutable" version="9.0.0" targetFramework="net462" /> <package id="System.Collections.Immutable" version="8.0.0" targetFramework="net462" />
<package id="System.ComponentModel.Annotations" version="4.7.0" targetFramework="net462" /> <package id="System.ComponentModel.Annotations" version="4.7.0" targetFramework="net462" />
<package id="System.Data.Common" version="4.3.0" targetFramework="net462" /> <package id="System.Data.Common" version="4.3.0" targetFramework="net462" />
<package id="System.Data.Odbc" version="6.0.1" targetFramework="net462" /> <package id="System.Data.Odbc" version="6.0.1" targetFramework="net462" />
@@ -74,23 +74,23 @@
<package id="System.Formats.Asn1" version="10.0.3" targetFramework="net462" /> <package id="System.Formats.Asn1" version="10.0.3" targetFramework="net462" />
<package id="System.IdentityModel.Tokens.Jwt" version="7.7.1" targetFramework="net462" /> <package id="System.IdentityModel.Tokens.Jwt" version="7.7.1" targetFramework="net462" />
<package id="System.IO.FileSystem.AccessControl" version="5.0.0" targetFramework="net462" /> <package id="System.IO.FileSystem.AccessControl" version="5.0.0" targetFramework="net462" />
<package id="System.IO.Packaging" version="9.0.0" targetFramework="net462" /> <package id="System.IO.Packaging" version="8.0.1" targetFramework="net462" />
<package id="System.IO.Pipelines" version="9.0.0" targetFramework="net462" /> <package id="System.IO.Pipelines" version="9.0.0" targetFramework="net462" />
<package id="System.Management" version="9.0.0" targetFramework="net462" /> <package id="System.Management" version="8.0.0" targetFramework="net462" />
<package id="System.Memory" version="4.6.3" targetFramework="net462" /> <package id="System.Memory" version="4.6.0" targetFramework="net462" />
<package id="System.Memory.Data" version="8.0.1" targetFramework="net462" /> <package id="System.Memory.Data" version="8.0.1" targetFramework="net462" />
<package id="System.Numerics.Vectors" version="4.6.1" targetFramework="net462" /> <package id="System.Numerics.Vectors" version="4.6.0" targetFramework="net462" />
<package id="System.Runtime.CompilerServices.Unsafe" version="6.1.2" targetFramework="net462" /> <package id="System.Runtime.CompilerServices.Unsafe" version="6.1.0" targetFramework="net462" />
<package id="System.Security.AccessControl" version="6.0.1" targetFramework="net462" /> <package id="System.Security.AccessControl" version="6.0.1" targetFramework="net462" />
<package id="System.Security.Cryptography.Algorithms" version="4.3.1" targetFramework="net462" /> <package id="System.Security.Cryptography.Algorithms" version="4.3.1" targetFramework="net462" />
<package id="System.Security.Cryptography.Cng" version="5.0.0" targetFramework="net462" /> <package id="System.Security.Cryptography.Cng" version="5.0.0" targetFramework="net462" />
<package id="System.Security.Cryptography.Pkcs" version="9.0.0" targetFramework="net462" /> <package id="System.Security.Cryptography.Pkcs" version="8.0.1" targetFramework="net462" />
<package id="System.Security.Cryptography.Primitives" version="4.3.0" targetFramework="net462" /> <package id="System.Security.Cryptography.Primitives" version="4.3.0" targetFramework="net462" />
<package id="System.Security.Cryptography.ProtectedData" version="4.5.0" targetFramework="net462" /> <package id="System.Security.Cryptography.ProtectedData" version="4.5.0" targetFramework="net462" />
<package id="System.Security.Principal.Windows" version="5.0.0" targetFramework="net462" /> <package id="System.Security.Principal.Windows" version="5.0.0" targetFramework="net462" />
<package id="System.Text.Encodings.Web" version="9.0.0" targetFramework="net462" /> <package id="System.Text.Encodings.Web" version="8.0.0" targetFramework="net462" />
<package id="System.Text.Json" version="9.0.0" targetFramework="net462" /> <package id="System.Text.Json" version="8.0.6" targetFramework="net462" />
<package id="System.Text.RegularExpressions" version="4.3.1" targetFramework="net462" /> <package id="System.Text.RegularExpressions" version="4.3.1" targetFramework="net462" />
<package id="System.Threading.Tasks.Extensions" version="4.6.0" targetFramework="net462" /> <package id="System.Threading.Tasks.Extensions" version="4.5.4" targetFramework="net462" />
<package id="System.ValueTuple" version="4.6.1" targetFramework="net462" /> <package id="System.ValueTuple" version="4.5.0" targetFramework="net462" />
</packages> </packages>

View File

@@ -230,6 +230,29 @@ Namespace Jobs
Return Task.FromResult(True) Return Task.FromResult(True)
End Function End Function
#Region "From BBTests"
Private Function ReadEnvelope(pEnvID As Integer) As Byte()
Dim strSql As String = "Select [BYTE_DATA] from [TBSIG_ENVELOPE_DOCUMENT] WHERE ENVELOPE_ID = " & pEnvID
Dim obyteDB = Database.GetScalarValue(strSql)
If Not IsDBNull(obyteDB) Then
Dim fileData As Byte() = DirectCast(Database.GetScalarValue(strSql), Byte())
If fileData IsNot Nothing Then
Return fileData
End If
End If
Throw New InvalidOperationException($"Byte data is null. Envelope ID: {pEnvID}")
End Function
Private Function LoadAnnotationDataForEnvelope(pEnvID As Integer) As DataTable
Dim oSql = $"SELECT VALUE FROM [TBSIG_DOCUMENT_STATUS] WHERE ENVELOPE_ID = {pEnvID}"
Return Database.GetDatatable(oSql)
End Function
#End Region
Private Sub Update_File_DB(pFilePath As String, pEnvelopeID As Long) Private Sub Update_File_DB(pFilePath As String, pEnvelopeID As Long)
Dim SqlCom As SqlCommand Dim SqlCom As SqlCommand
Dim imageData As Byte() Dim imageData As Byte()
@@ -418,7 +441,18 @@ Namespace Jobs
End Try End Try
End If End If
Return PDFBurner.BurnAnnotsToPDF(oInputDocumentBuffer, oAnnotations, pEnvelopeData.EnvelopeId) #Region "From BBTests"
Dim oTable = LoadAnnotationDataForEnvelope(pEnvelopeId)
Dim oJsonList = oTable.Rows.
Cast(Of DataRow).
Select(Function(r As DataRow) r.Item("VALUE").ToString()).
ToList()
Dim oBuffer As Byte() = ReadEnvelope(pEnvelopeId)
#End Region
Return PDFBurner.BurnAnnotsToPDF(oBuffer, oJsonList, pEnvelopeId)
End Function End Function
Private Function GetEnvelopeData(pEnvelopeId As Integer) As EnvelopeData Private Function GetEnvelopeData(pEnvelopeId As Integer) As EnvelopeData

View File

@@ -85,16 +85,37 @@ Namespace Jobs.FinalizeDocument
'Add annotations 'Add annotations
For Each element In elements For Each element In elements
If element Is Nothing Then
Continue For
End If
Dim elementAnnotations = If(element.Annotations, Enumerable.Empty(Of ElementAnnotation)())
If Not elementAnnotations.Any() Then
Continue For
End If
Dim frameX = (element.Left - 0.7 - margin) Dim frameX = (element.Left - 0.7 - margin)
Dim frame = element.Annotations.FirstOrDefault(Function(a) a.Name = "frame") Dim frame = elementAnnotations.FirstOrDefault(Function(a) a.Name = "frame")
Dim frameY = element.Top - 0.5 - margin Dim frameY = element.Top - 0.5 - margin
Dim frameYShift = frame.Y - frameY * inchFactor Dim frameYShift As Double = 0
Dim frameXShift = frame.X - frameX * inchFactor Dim frameXShift As Double = 0
If frame IsNot Nothing Then
frameYShift = frame.Y - frameY * inchFactor
frameXShift = frame.X - frameX * inchFactor
End If
For Each annot In elementAnnotations
If annot Is Nothing Then
Continue For
End If
Dim yOffsetofFF As Double = 0
If Not String.IsNullOrEmpty(annot.Name) Then
yOffsetsOfFF.TryGetValue(annot.Name, yOffsetofFF)
End If
For Each annot In element.Annotations
Dim yOffsetofFF As Double = If(yOffsetsOfFF.TryGetValue(annot.Name, yOffsetofFF), yOffsetofFF, 0)
Dim y = frameY + yOffsetofFF Dim y = frameY + yOffsetofFF
If annot.Type = AnnotationType.FormField Then If annot.Type = AnnotationType.FormField Then

View File

@@ -56,7 +56,7 @@ Public Class PDFMerger
' Convert to PDF/A ' Convert to PDF/A
oMergedPDF.ConvertToPDFA(oFinalStream, PDFAConformanceLevel, ALLOW_VECTORIZATION, ALLOW_RASTERIZATION) oMergedPDF.ConvertToPDFA(oFinalStream, PDFAConformanceLevel, ALLOW_VECTORIZATION, ALLOW_RASTERIZATION)
oStatus = oDocumentPDF.GetStat() oStatus = oMergedPDF.GetStat()
If oStatus <> GdPictureStatus.OK Then If oStatus <> GdPictureStatus.OK Then
Throw New MergeDocumentException($"Document could not be converted to PDF/A: {oStatus}") Throw New MergeDocumentException($"Document could not be converted to PDF/A: {oStatus}")
End If End If

View File

@@ -270,12 +270,26 @@ Public Class ReceiverModel
Private Function GetSignedDate(pEmailAddress As String, pEnvelopeId As Integer) As Date Private Function GetSignedDate(pEmailAddress As String, pEnvelopeId As Integer) As Date
Try Try
Dim oStatusInt As Integer = EnvelopeStatus.DocumentSigned Dim oStatusInt As Integer = EnvelopeStatus.DocumentSigned
Return Database.GetScalarValue($"SELECT ACTION_DATE FROM [DD_ECM].[dbo].[TBSIG_ENVELOPE_HISTORY] WHERE ENVELOPE_ID = {pEnvelopeId} Dim value = Database.GetScalarValue($"SELECT ACTION_DATE FROM [DD_ECM].[dbo].[TBSIG_ENVELOPE_HISTORY] WHERE ENVELOPE_ID = {pEnvelopeId}
And USER_REFERENCE = '{pEmailAddress}' AND [STATUS] = {oStatusInt}") And USER_REFERENCE = '{pEmailAddress}' AND [STATUS] = {oStatusInt}")
If value Is Nothing OrElse value Is DBNull.Value Then
Return DateTime.MinValue
End If
If TypeOf value Is DateTime Then
Return DirectCast(value, DateTime)
End If
Dim parsedDate As DateTime
If DateTime.TryParse(value.ToString(), parsedDate) Then
Return parsedDate
End If
Return DateTime.MinValue
Catch ex As Exception Catch ex As Exception
Logger.Error(ex) Logger.Error(ex)
Return Nothing Return DateTime.MinValue
End Try End Try
End Function End Function

View File

@@ -1,10 +1,10 @@
'------------------------------------------------------------------------------ '------------------------------------------------------------------------------
' <auto-generated> ' <auto-generated>
' Dieser Code wurde von einem Tool generiert. ' This code was generated by a tool.
' Laufzeitversion:4.0.30319.42000 ' Runtime Version:4.0.30319.42000
' '
' Änderungen an dieser Datei können falsches Verhalten verursachen und gehen verloren, wenn ' Changes to this file may cause incorrect behavior and will be lost if
' der Code erneut generiert wird. ' the code is regenerated.
' </auto-generated> ' </auto-generated>
'------------------------------------------------------------------------------ '------------------------------------------------------------------------------
@@ -15,12 +15,12 @@ Imports System
Namespace My.Resources Namespace My.Resources
'Diese Klasse wurde von der StronglyTypedResourceBuilder automatisch generiert 'This class was auto-generated by the StronglyTypedResourceBuilder
'-Klasse über ein Tool wie ResGen oder Visual Studio automatisch generiert. 'class via a tool like ResGen or Visual Studio.
'Um einen Member hinzuzufügen oder zu entfernen, bearbeiten Sie die .ResX-Datei und führen dann ResGen 'To add or remove a member, edit your .ResX file then rerun ResGen
'mit der /str-Option erneut aus, oder Sie erstellen Ihr VS-Projekt neu. 'with the /str option, or rebuild your VS project.
'''<summary> '''<summary>
''' Eine stark typisierte Ressourcenklasse zum Suchen von lokalisierten Zeichenfolgen usw. ''' A strongly-typed resource class, for looking up localized strings, etc.
'''</summary> '''</summary>
<Global.System.CodeDom.Compiler.GeneratedCodeAttribute("System.Resources.Tools.StronglyTypedResourceBuilder", "17.0.0.0"), _ <Global.System.CodeDom.Compiler.GeneratedCodeAttribute("System.Resources.Tools.StronglyTypedResourceBuilder", "17.0.0.0"), _
Global.System.Diagnostics.DebuggerNonUserCodeAttribute(), _ Global.System.Diagnostics.DebuggerNonUserCodeAttribute(), _
@@ -37,7 +37,7 @@ Namespace My.Resources
End Sub End Sub
'''<summary> '''<summary>
''' Gibt die zwischengespeicherte ResourceManager-Instanz zurück, die von dieser Klasse verwendet wird. ''' Returns the cached ResourceManager instance used by this class.
'''</summary> '''</summary>
<Global.System.ComponentModel.EditorBrowsableAttribute(Global.System.ComponentModel.EditorBrowsableState.Advanced)> _ <Global.System.ComponentModel.EditorBrowsableAttribute(Global.System.ComponentModel.EditorBrowsableState.Advanced)> _
Public Shared ReadOnly Property ResourceManager() As Global.System.Resources.ResourceManager Public Shared ReadOnly Property ResourceManager() As Global.System.Resources.ResourceManager
@@ -51,8 +51,8 @@ Namespace My.Resources
End Property End Property
'''<summary> '''<summary>
''' Überschreibt die CurrentUICulture-Eigenschaft des aktuellen Threads für alle ''' Overrides the current thread's CurrentUICulture property for all
''' Ressourcenzuordnungen, die diese stark typisierte Ressourcenklasse verwenden. ''' resource lookups using this strongly typed resource class.
'''</summary> '''</summary>
<Global.System.ComponentModel.EditorBrowsableAttribute(Global.System.ComponentModel.EditorBrowsableState.Advanced)> _ <Global.System.ComponentModel.EditorBrowsableAttribute(Global.System.ComponentModel.EditorBrowsableState.Advanced)> _
Public Shared Property Culture() As Global.System.Globalization.CultureInfo Public Shared Property Culture() As Global.System.Globalization.CultureInfo
@@ -65,7 +65,7 @@ Namespace My.Resources
End Property End Property
'''<summary> '''<summary>
''' Sucht eine lokalisierte Zeichenfolge, die Zugriffscode korrekt eingegeben ähnelt. ''' Looks up a localized string similar to Zugriffscode korrekt eingegeben.
'''</summary> '''</summary>
Public Shared ReadOnly Property AccessCodeCorrect() As String Public Shared ReadOnly Property AccessCodeCorrect() As String
Get Get
@@ -74,7 +74,7 @@ Namespace My.Resources
End Property End Property
'''<summary> '''<summary>
''' Sucht eine lokalisierte Zeichenfolge, die Zugriffscode falsch eingegeben ähnelt. ''' Looks up a localized string similar to Zugriffscode falsch eingegeben.
'''</summary> '''</summary>
Public Shared ReadOnly Property AccessCodeIncorrect() As String Public Shared ReadOnly Property AccessCodeIncorrect() As String
Get Get
@@ -83,7 +83,7 @@ Namespace My.Resources
End Property End Property
'''<summary> '''<summary>
''' Sucht eine lokalisierte Zeichenfolge, die Zugriffscode angefordert ähnelt. ''' Looks up a localized string similar to Zugriffscode angefordert.
'''</summary> '''</summary>
Public Shared ReadOnly Property AccessCodeRequested() As String Public Shared ReadOnly Property AccessCodeRequested() As String
Get Get
@@ -92,7 +92,7 @@ Namespace My.Resources
End Property End Property
'''<summary> '''<summary>
''' Sucht eine lokalisierte Zeichenfolge, die Fortgeschrittene Elektronische Signatur ähnelt. ''' Looks up a localized string similar to Fortgeschrittene Elektronische Signatur.
'''</summary> '''</summary>
Public Shared ReadOnly Property AdvancedElectronicSignature() As String Public Shared ReadOnly Property AdvancedElectronicSignature() As String
Get Get
@@ -101,7 +101,7 @@ Namespace My.Resources
End Property End Property
'''<summary> '''<summary>
''' Sucht eine lokalisierte Zeichenfolge, die Abgeschlossen ähnelt. ''' Looks up a localized string similar to Abgeschlossen.
'''</summary> '''</summary>
Public Shared ReadOnly Property Completed() As String Public Shared ReadOnly Property Completed() As String
Get Get
@@ -110,7 +110,16 @@ Namespace My.Resources
End Property End Property
'''<summary> '''<summary>
''' Sucht eine lokalisierte Zeichenfolge, die Vollständig Signiert ähnelt. ''' Looks up a localized string similar to Vollständig bestätigt.
'''</summary>
Public Shared ReadOnly Property CompletelyConfirmed() As String
Get
Return ResourceManager.GetString("CompletelyConfirmed", resourceCulture)
End Get
End Property
'''<summary>
''' Looks up a localized string similar to Vollständig signiert.
'''</summary> '''</summary>
Public Shared ReadOnly Property CompletelySigned() As String Public Shared ReadOnly Property CompletelySigned() As String
Get Get
@@ -119,7 +128,25 @@ Namespace My.Resources
End Property End Property
'''<summary> '''<summary>
''' Sucht eine lokalisierte Zeichenfolge, die Vertrag ähnelt. ''' Looks up a localized string similar to Lesebestätigung.
'''</summary>
Public Shared ReadOnly Property Confirmation() As String
Get
Return ResourceManager.GetString("Confirmation", resourceCulture)
End Get
End Property
'''<summary>
''' Looks up a localized string similar to Gelesen und Bestätigt.
'''</summary>
Public Shared ReadOnly Property Confirmed() As String
Get
Return ResourceManager.GetString("Confirmed", resourceCulture)
End Get
End Property
'''<summary>
''' Looks up a localized string similar to Vertrag.
'''</summary> '''</summary>
Public Shared ReadOnly Property Contract() As String Public Shared ReadOnly Property Contract() As String
Get Get
@@ -128,7 +155,7 @@ Namespace My.Resources
End Property End Property
'''<summary> '''<summary>
''' Sucht eine lokalisierte Zeichenfolge, die Erstellt ähnelt. ''' Looks up a localized string similar to Erstellt.
'''</summary> '''</summary>
Public Shared ReadOnly Property Created() As String Public Shared ReadOnly Property Created() As String
Get Get
@@ -137,7 +164,16 @@ Namespace My.Resources
End Property End Property
'''<summary> '''<summary>
''' Sucht eine lokalisierte Zeichenfolge, die Dokument Rotation geändert ähnelt. ''' Looks up a localized string similar to Dokument gelesen und bestätigt.
'''</summary>
Public Shared ReadOnly Property DocumentConfirmed() As String
Get
Return ResourceManager.GetString("DocumentConfirmed", resourceCulture)
End Get
End Property
'''<summary>
''' Looks up a localized string similar to Dokument Rotation geändert.
'''</summary> '''</summary>
Public Shared ReadOnly Property DocumentMod_Rotation() As String Public Shared ReadOnly Property DocumentMod_Rotation() As String
Get Get
@@ -146,7 +182,7 @@ Namespace My.Resources
End Property End Property
'''<summary> '''<summary>
''' Sucht eine lokalisierte Zeichenfolge, die Dokument geöffnet ähnelt. ''' Looks up a localized string similar to Dokument geöffnet.
'''</summary> '''</summary>
Public Shared ReadOnly Property DocumentOpened() As String Public Shared ReadOnly Property DocumentOpened() As String
Get Get
@@ -155,7 +191,7 @@ Namespace My.Resources
End Property End Property
'''<summary> '''<summary>
''' Sucht eine lokalisierte Zeichenfolge, die Unterzeichnung abgelehnt ähnelt. ''' Looks up a localized string similar to Unterzeichnung abgelehnt.
'''</summary> '''</summary>
Public Shared ReadOnly Property DocumentRejected() As String Public Shared ReadOnly Property DocumentRejected() As String
Get Get
@@ -164,7 +200,16 @@ Namespace My.Resources
End Property End Property
'''<summary> '''<summary>
''' Sucht eine lokalisierte Zeichenfolge, die Dokument unterzeichnet ähnelt. ''' Looks up a localized string similar to Lesebestätigung abgelehnt.
'''</summary>
Public Shared ReadOnly Property DocumentRejectedRaC() As String
Get
Return ResourceManager.GetString("DocumentRejectedRaC", resourceCulture)
End Get
End Property
'''<summary>
''' Looks up a localized string similar to Dokument unterzeichnet.
'''</summary> '''</summary>
Public Shared ReadOnly Property DocumentSigned() As String Public Shared ReadOnly Property DocumentSigned() As String
Get Get
@@ -173,7 +218,7 @@ Namespace My.Resources
End Property End Property
'''<summary> '''<summary>
''' Sucht eine lokalisierte Zeichenfolge, die Entwurf ähnelt. ''' Looks up a localized string similar to Entwurf.
'''</summary> '''</summary>
Public Shared ReadOnly Property Draft() As String Public Shared ReadOnly Property Draft() As String
Get Get
@@ -182,7 +227,7 @@ Namespace My.Resources
End Property End Property
'''<summary> '''<summary>
''' Sucht eine lokalisierte Zeichenfolge, die Archiviert ähnelt. ''' Looks up a localized string similar to Archiviert.
'''</summary> '''</summary>
Public Shared ReadOnly Property EnvelopeArchived() As String Public Shared ReadOnly Property EnvelopeArchived() As String
Get Get
@@ -191,7 +236,16 @@ Namespace My.Resources
End Property End Property
'''<summary> '''<summary>
''' Sucht eine lokalisierte Zeichenfolge, die Vollständig signiert ähnelt. ''' Looks up a localized string similar to Vollständig bestätigt.
'''</summary>
Public Shared ReadOnly Property EnvelopeCompletelyConfirmed() As String
Get
Return ResourceManager.GetString("EnvelopeCompletelyConfirmed", resourceCulture)
End Get
End Property
'''<summary>
''' Looks up a localized string similar to Vollständig signiert.
'''</summary> '''</summary>
Public Shared ReadOnly Property EnvelopeCompletelySigned() As String Public Shared ReadOnly Property EnvelopeCompletelySigned() As String
Get Get
@@ -200,7 +254,7 @@ Namespace My.Resources
End Property End Property
'''<summary> '''<summary>
''' Sucht eine lokalisierte Zeichenfolge, die Umschlag Erstellt ähnelt. ''' Looks up a localized string similar to Umschlag Erstellt.
'''</summary> '''</summary>
Public Shared ReadOnly Property EnvelopeCreated() As String Public Shared ReadOnly Property EnvelopeCreated() As String
Get Get
@@ -209,7 +263,7 @@ Namespace My.Resources
End Property End Property
'''<summary> '''<summary>
''' Sucht eine lokalisierte Zeichenfolge, die Umschlag Gelöscht ähnelt. ''' Looks up a localized string similar to Umschlag Gelöscht.
'''</summary> '''</summary>
Public Shared ReadOnly Property EnvelopeDeleted() As String Public Shared ReadOnly Property EnvelopeDeleted() As String
Get Get
@@ -218,7 +272,16 @@ Namespace My.Resources
End Property End Property
'''<summary> '''<summary>
''' Sucht eine lokalisierte Zeichenfolge, die Teil-Signiert ähnelt. ''' Looks up a localized string similar to Teil-Bestätigt.
'''</summary>
Public Shared ReadOnly Property EnvelopePartlyConfirmed() As String
Get
Return ResourceManager.GetString("EnvelopePartlyConfirmed", resourceCulture)
End Get
End Property
'''<summary>
''' Looks up a localized string similar to Teil-Signiert.
'''</summary> '''</summary>
Public Shared ReadOnly Property EnvelopePartlySigned() As String Public Shared ReadOnly Property EnvelopePartlySigned() As String
Get Get
@@ -227,7 +290,7 @@ Namespace My.Resources
End Property End Property
'''<summary> '''<summary>
''' Sucht eine lokalisierte Zeichenfolge, die Umschlag in Queue ähnelt. ''' Looks up a localized string similar to Umschlag in Queue.
'''</summary> '''</summary>
Public Shared ReadOnly Property EnvelopeQueued() As String Public Shared ReadOnly Property EnvelopeQueued() As String
Get Get
@@ -236,7 +299,7 @@ Namespace My.Resources
End Property End Property
'''<summary> '''<summary>
''' Sucht eine lokalisierte Zeichenfolge, die Umschlag abgelehnt ähnelt. ''' Looks up a localized string similar to Umschlag abgelehnt.
'''</summary> '''</summary>
Public Shared ReadOnly Property EnvelopeRejected() As String Public Shared ReadOnly Property EnvelopeRejected() As String
Get Get
@@ -245,7 +308,7 @@ Namespace My.Resources
End Property End Property
'''<summary> '''<summary>
''' Sucht eine lokalisierte Zeichenfolge, die Signierungszertifikat erstellt ähnelt. ''' Looks up a localized string similar to Signierungszertifikat erstellt.
'''</summary> '''</summary>
Public Shared ReadOnly Property EnvelopeReportCreated() As String Public Shared ReadOnly Property EnvelopeReportCreated() As String
Get Get
@@ -254,7 +317,16 @@ Namespace My.Resources
End Property End Property
'''<summary> '''<summary>
''' Sucht eine lokalisierte Zeichenfolge, die Gespeichert ähnelt. ''' Looks up a localized string similar to Lesebestätigungszertifikat erstellt.
'''</summary>
Public Shared ReadOnly Property EnvelopeReportCreatedRaC() As String
Get
Return ResourceManager.GetString("EnvelopeReportCreatedRaC", resourceCulture)
End Get
End Property
'''<summary>
''' Looks up a localized string similar to Gespeichert.
'''</summary> '''</summary>
Public Shared ReadOnly Property EnvelopeSaved() As String Public Shared ReadOnly Property EnvelopeSaved() As String
Get Get
@@ -263,7 +335,7 @@ Namespace My.Resources
End Property End Property
'''<summary> '''<summary>
''' Sucht eine lokalisierte Zeichenfolge, die Gesendet ähnelt. ''' Looks up a localized string similar to Gesendet.
'''</summary> '''</summary>
Public Shared ReadOnly Property EnvelopeSent() As String Public Shared ReadOnly Property EnvelopeSent() As String
Get Get
@@ -272,7 +344,7 @@ Namespace My.Resources
End Property End Property
'''<summary> '''<summary>
''' Sucht eine lokalisierte Zeichenfolge, die Umschlag zurückgezogen ähnelt. ''' Looks up a localized string similar to Umschlag zurückgezogen.
'''</summary> '''</summary>
Public Shared ReadOnly Property EnvelopeWithdrawn() As String Public Shared ReadOnly Property EnvelopeWithdrawn() As String
Get Get
@@ -281,7 +353,7 @@ Namespace My.Resources
End Property End Property
'''<summary> '''<summary>
''' Sucht eine lokalisierte Zeichenfolge, die Zugriffscode versendet ähnelt. ''' Looks up a localized string similar to Zugriffscode versendet.
'''</summary> '''</summary>
Public Shared ReadOnly Property MessageAccessCodeSent() As String Public Shared ReadOnly Property MessageAccessCodeSent() As String
Get Get
@@ -290,7 +362,7 @@ Namespace My.Resources
End Property End Property
'''<summary> '''<summary>
''' Sucht eine lokalisierte Zeichenfolge, die Abschlussemail versendet ähnelt. ''' Looks up a localized string similar to Abschlussemail versendet.
'''</summary> '''</summary>
Public Shared ReadOnly Property MessageCompletionSent() As String Public Shared ReadOnly Property MessageCompletionSent() As String
Get Get
@@ -299,7 +371,7 @@ Namespace My.Resources
End Property End Property
'''<summary> '''<summary>
''' Sucht eine lokalisierte Zeichenfolge, die Signaturbestätigung versendet ähnelt. ''' Looks up a localized string similar to Signaturbestätigung versendet.
'''</summary> '''</summary>
Public Shared ReadOnly Property MessageConfirmationSent() As String Public Shared ReadOnly Property MessageConfirmationSent() As String
Get Get
@@ -308,7 +380,16 @@ Namespace My.Resources
End Property End Property
'''<summary> '''<summary>
''' Sucht eine lokalisierte Zeichenfolge, die Löschinformation versendet ähnelt. ''' Looks up a localized string similar to Lesebestätigung versendet.
'''</summary>
Public Shared ReadOnly Property MessageConfirmationSentRaC() As String
Get
Return ResourceManager.GetString("MessageConfirmationSentRaC", resourceCulture)
End Get
End Property
'''<summary>
''' Looks up a localized string similar to Löschinformation versendet.
'''</summary> '''</summary>
Public Shared ReadOnly Property MessageDeletionSent() As String Public Shared ReadOnly Property MessageDeletionSent() As String
Get Get
@@ -317,7 +398,7 @@ Namespace My.Resources
End Property End Property
'''<summary> '''<summary>
''' Sucht eine lokalisierte Zeichenfolge, die Dokumentenlink versendet ähnelt. ''' Looks up a localized string similar to Dokumentenlink versendet.
'''</summary> '''</summary>
Public Shared ReadOnly Property MessageInvitationSent() As String Public Shared ReadOnly Property MessageInvitationSent() As String
Get Get
@@ -326,7 +407,7 @@ Namespace My.Resources
End Property End Property
'''<summary> '''<summary>
''' Sucht eine lokalisierte Zeichenfolge, die Nein ähnelt. ''' Looks up a localized string similar to Nein.
'''</summary> '''</summary>
Public Shared ReadOnly Property No() As String Public Shared ReadOnly Property No() As String
Get Get
@@ -335,7 +416,16 @@ Namespace My.Resources
End Property End Property
'''<summary> '''<summary>
''' Sucht eine lokalisierte Zeichenfolge, die Teil-Signiert ähnelt. ''' Looks up a localized string similar to Teil-Bestätigt.
'''</summary>
Public Shared ReadOnly Property PartlyConfirmed() As String
Get
Return ResourceManager.GetString("PartlyConfirmed", resourceCulture)
End Get
End Property
'''<summary>
''' Looks up a localized string similar to Teil-Signiert.
'''</summary> '''</summary>
Public Shared ReadOnly Property PartlySigned() As String Public Shared ReadOnly Property PartlySigned() As String
Get Get
@@ -344,7 +434,7 @@ Namespace My.Resources
End Property End Property
'''<summary> '''<summary>
''' Sucht eine lokalisierte Zeichenfolge, die Qualifizierte Signatur ähnelt. ''' Looks up a localized string similar to Qualifizierte Signatur.
'''</summary> '''</summary>
Public Shared ReadOnly Property QualifiedSignature() As String Public Shared ReadOnly Property QualifiedSignature() As String
Get Get
@@ -353,7 +443,7 @@ Namespace My.Resources
End Property End Property
'''<summary> '''<summary>
''' Sucht eine lokalisierte Zeichenfolge, die Arbeitsanweisung ähnelt. ''' Looks up a localized string similar to Lesebestätigung.
'''</summary> '''</summary>
Public Shared ReadOnly Property ReadAndSign() As String Public Shared ReadOnly Property ReadAndSign() As String
Get Get
@@ -362,7 +452,7 @@ Namespace My.Resources
End Property End Property
'''<summary> '''<summary>
''' Sucht eine lokalisierte Zeichenfolge, die Wollen Sie die 2-Faktor Definition für diesen Empfänger zurücksetzen. Der Empfänger muss sich dann neu identifizieren! ähnelt. ''' Looks up a localized string similar to Wollen Sie die 2-Faktor Definition für diesen Empfänger zurücksetzen. Der Empfänger muss sich dann neu identifizieren!.
'''</summary> '''</summary>
Public Shared ReadOnly Property ResetTOTPUser() As String Public Shared ReadOnly Property ResetTOTPUser() As String
Get Get
@@ -371,7 +461,7 @@ Namespace My.Resources
End Property End Property
'''<summary> '''<summary>
''' Sucht eine lokalisierte Zeichenfolge, die Gespeichert ähnelt. ''' Looks up a localized string similar to Gespeichert.
'''</summary> '''</summary>
Public Shared ReadOnly Property Saved() As String Public Shared ReadOnly Property Saved() As String
Get Get
@@ -380,7 +470,7 @@ Namespace My.Resources
End Property End Property
'''<summary> '''<summary>
''' Sucht eine lokalisierte Zeichenfolge, die Gesendet ähnelt. ''' Looks up a localized string similar to Gesendet.
'''</summary> '''</summary>
Public Shared ReadOnly Property Sent() As String Public Shared ReadOnly Property Sent() As String
Get Get
@@ -389,7 +479,7 @@ Namespace My.Resources
End Property End Property
'''<summary> '''<summary>
''' Sucht eine lokalisierte Zeichenfolge, die Signatur ähnelt. ''' Looks up a localized string similar to Signatur.
'''</summary> '''</summary>
Public Shared ReadOnly Property Signature() As String Public Shared ReadOnly Property Signature() As String
Get Get
@@ -398,7 +488,7 @@ Namespace My.Resources
End Property End Property
'''<summary> '''<summary>
''' Sucht eine lokalisierte Zeichenfolge, die Signatur bestätigt ähnelt. ''' Looks up a localized string similar to Abschluss bestätigt.
'''</summary> '''</summary>
Public Shared ReadOnly Property SignatureConfirmed() As String Public Shared ReadOnly Property SignatureConfirmed() As String
Get Get
@@ -407,7 +497,7 @@ Namespace My.Resources
End Property End Property
'''<summary> '''<summary>
''' Sucht eine lokalisierte Zeichenfolge, die Signiert ähnelt. ''' Looks up a localized string similar to Signiert.
'''</summary> '''</summary>
Public Shared ReadOnly Property Signed() As String Public Shared ReadOnly Property Signed() As String
Get Get
@@ -416,7 +506,7 @@ Namespace My.Resources
End Property End Property
'''<summary> '''<summary>
''' Sucht eine lokalisierte Zeichenfolge, die Erfolgreich! Dialog wird geschlossen. ähnelt. ''' Looks up a localized string similar to Erfolgreich! Dialog wird geschlossen..
'''</summary> '''</summary>
Public Shared ReadOnly Property Success_FormClose() As String Public Shared ReadOnly Property Success_FormClose() As String
Get Get
@@ -425,7 +515,7 @@ Namespace My.Resources
End Property End Property
'''<summary> '''<summary>
''' Sucht eine lokalisierte Zeichenfolge, die Unsigniert ähnelt. ''' Looks up a localized string similar to Unsigniert.
'''</summary> '''</summary>
Public Shared ReadOnly Property Unsigned() As String Public Shared ReadOnly Property Unsigned() As String
Get Get
@@ -434,7 +524,7 @@ Namespace My.Resources
End Property End Property
'''<summary> '''<summary>
''' Sucht eine lokalisierte Zeichenfolge, die Ja ähnelt. ''' Looks up a localized string similar to Ja.
'''</summary> '''</summary>
Public Shared ReadOnly Property Yes() As String Public Shared ReadOnly Property Yes() As String
Get Get
@@ -443,7 +533,7 @@ Namespace My.Resources
End Property End Property
'''<summary> '''<summary>
''' Sucht eine lokalisierte Zeichenfolge, die Ja, mit Anhang ähnelt. ''' Looks up a localized string similar to Ja, mit Anhang.
'''</summary> '''</summary>
Public Shared ReadOnly Property YesWithAttachment() As String Public Shared ReadOnly Property YesWithAttachment() As String
Get Get

View File

@@ -132,12 +132,27 @@
<data name="Completed" xml:space="preserve"> <data name="Completed" xml:space="preserve">
<value>Completed</value> <value>Completed</value>
</data> </data>
<data name="CompletelyConfirmed" xml:space="preserve">
<value>Completely confirmed</value>
</data>
<data name="CompletelySigned" xml:space="preserve">
<value>Completely signed</value>
</data>
<data name="Confirmation" xml:space="preserve">
<value>Read Confirmation</value>
</data>
<data name="Confirmed" xml:space="preserve">
<value>Read and signed</value>
</data>
<data name="Contract" xml:space="preserve"> <data name="Contract" xml:space="preserve">
<value>Contract</value> <value>Contract</value>
</data> </data>
<data name="Created" xml:space="preserve"> <data name="Created" xml:space="preserve">
<value>Created</value> <value>Created</value>
</data> </data>
<data name="DocumentConfirmed" xml:space="preserve">
<value>Document read and confirmed</value>
</data>
<data name="DocumentMod_Rotation" xml:space="preserve"> <data name="DocumentMod_Rotation" xml:space="preserve">
<value>Document rotation adapted</value> <value>Document rotation adapted</value>
</data> </data>
@@ -147,6 +162,9 @@
<data name="DocumentRejected" xml:space="preserve"> <data name="DocumentRejected" xml:space="preserve">
<value>Signing rejected</value> <value>Signing rejected</value>
</data> </data>
<data name="DocumentRejectedRaC" xml:space="preserve">
<value>Read confirmation rejected</value>
</data>
<data name="DocumentSigned" xml:space="preserve"> <data name="DocumentSigned" xml:space="preserve">
<value>Document signed</value> <value>Document signed</value>
</data> </data>
@@ -156,6 +174,9 @@
<data name="EnvelopeArchived" xml:space="preserve"> <data name="EnvelopeArchived" xml:space="preserve">
<value>Archived</value> <value>Archived</value>
</data> </data>
<data name="EnvelopeCompletelyConfirmed" xml:space="preserve">
<value>Completely confirmed</value>
</data>
<data name="EnvelopeCompletelySigned" xml:space="preserve"> <data name="EnvelopeCompletelySigned" xml:space="preserve">
<value>Completely signed</value> <value>Completely signed</value>
</data> </data>
@@ -165,8 +186,11 @@
<data name="EnvelopeDeleted" xml:space="preserve"> <data name="EnvelopeDeleted" xml:space="preserve">
<value>Envelope Deleted</value> <value>Envelope Deleted</value>
</data> </data>
<data name="EnvelopePartlyConfirmed" xml:space="preserve">
<value>Partially confirmed</value>
</data>
<data name="EnvelopePartlySigned" xml:space="preserve"> <data name="EnvelopePartlySigned" xml:space="preserve">
<value>Partly signed</value> <value>Partially signed</value>
</data> </data>
<data name="EnvelopeQueued" xml:space="preserve"> <data name="EnvelopeQueued" xml:space="preserve">
<value>Envelope Queued</value> <value>Envelope Queued</value>
@@ -177,6 +201,9 @@
<data name="EnvelopeReportCreated" xml:space="preserve"> <data name="EnvelopeReportCreated" xml:space="preserve">
<value>Signature certificate created</value> <value>Signature certificate created</value>
</data> </data>
<data name="EnvelopeReportCreatedRaC" xml:space="preserve">
<value>Read Confirmation Certificate Created</value>
</data>
<data name="EnvelopeSaved" xml:space="preserve"> <data name="EnvelopeSaved" xml:space="preserve">
<value>Saved</value> <value>Saved</value>
</data> </data>
@@ -195,6 +222,9 @@
<data name="MessageConfirmationSent" xml:space="preserve"> <data name="MessageConfirmationSent" xml:space="preserve">
<value>Confirmation Sent</value> <value>Confirmation Sent</value>
</data> </data>
<data name="MessageConfirmationSentRaC" xml:space="preserve">
<value>Read Confirmation Sent</value>
</data>
<data name="MessageDeletionSent" xml:space="preserve"> <data name="MessageDeletionSent" xml:space="preserve">
<value>Deletion Notice Sent</value> <value>Deletion Notice Sent</value>
</data> </data>
@@ -204,6 +234,12 @@
<data name="No" xml:space="preserve"> <data name="No" xml:space="preserve">
<value>No</value> <value>No</value>
</data> </data>
<data name="PartlyConfirmed" xml:space="preserve">
<value>Partially confirmed</value>
</data>
<data name="PartlySigned" xml:space="preserve">
<value>Partially signed</value>
</data>
<data name="QualifiedSignature" xml:space="preserve"> <data name="QualifiedSignature" xml:space="preserve">
<value>Qualified Signature</value> <value>Qualified Signature</value>
</data> </data>
@@ -223,7 +259,7 @@
<value>Signature</value> <value>Signature</value>
</data> </data>
<data name="SignatureConfirmed" xml:space="preserve"> <data name="SignatureConfirmed" xml:space="preserve">
<value>Signature confirmed</value> <value>Finalization confirmed</value>
</data> </data>
<data name="Signed" xml:space="preserve"> <data name="Signed" xml:space="preserve">
<value>Signed</value> <value>Signed</value>

View File

@@ -132,8 +132,17 @@
<data name="Completed" xml:space="preserve"> <data name="Completed" xml:space="preserve">
<value>Abgeschlossen</value> <value>Abgeschlossen</value>
</data> </data>
<data name="CompletelyConfirmed" xml:space="preserve">
<value>Vollständig bestätigt</value>
</data>
<data name="CompletelySigned" xml:space="preserve"> <data name="CompletelySigned" xml:space="preserve">
<value>Vollständig Signiert</value> <value>Vollständig signiert</value>
</data>
<data name="Confirmation" xml:space="preserve">
<value>Lesebestätigung</value>
</data>
<data name="Confirmed" xml:space="preserve">
<value>Gelesen und Bestätigt</value>
</data> </data>
<data name="Contract" xml:space="preserve"> <data name="Contract" xml:space="preserve">
<value>Vertrag</value> <value>Vertrag</value>
@@ -141,6 +150,9 @@
<data name="Created" xml:space="preserve"> <data name="Created" xml:space="preserve">
<value>Erstellt</value> <value>Erstellt</value>
</data> </data>
<data name="DocumentConfirmed" xml:space="preserve">
<value>Dokument gelesen und bestätigt</value>
</data>
<data name="DocumentMod_Rotation" xml:space="preserve"> <data name="DocumentMod_Rotation" xml:space="preserve">
<value>Dokument Rotation geändert</value> <value>Dokument Rotation geändert</value>
</data> </data>
@@ -150,6 +162,9 @@
<data name="DocumentRejected" xml:space="preserve"> <data name="DocumentRejected" xml:space="preserve">
<value>Unterzeichnung abgelehnt</value> <value>Unterzeichnung abgelehnt</value>
</data> </data>
<data name="DocumentRejectedRaC" xml:space="preserve">
<value>Lesebestätigung abgelehnt</value>
</data>
<data name="DocumentSigned" xml:space="preserve"> <data name="DocumentSigned" xml:space="preserve">
<value>Dokument unterzeichnet</value> <value>Dokument unterzeichnet</value>
</data> </data>
@@ -159,6 +174,9 @@
<data name="EnvelopeArchived" xml:space="preserve"> <data name="EnvelopeArchived" xml:space="preserve">
<value>Archiviert</value> <value>Archiviert</value>
</data> </data>
<data name="EnvelopeCompletelyConfirmed" xml:space="preserve">
<value>Vollständig bestätigt</value>
</data>
<data name="EnvelopeCompletelySigned" xml:space="preserve"> <data name="EnvelopeCompletelySigned" xml:space="preserve">
<value>Vollständig signiert</value> <value>Vollständig signiert</value>
</data> </data>
@@ -168,6 +186,9 @@
<data name="EnvelopeDeleted" xml:space="preserve"> <data name="EnvelopeDeleted" xml:space="preserve">
<value>Umschlag Gelöscht</value> <value>Umschlag Gelöscht</value>
</data> </data>
<data name="EnvelopePartlyConfirmed" xml:space="preserve">
<value>Teil-Bestätigt</value>
</data>
<data name="EnvelopePartlySigned" xml:space="preserve"> <data name="EnvelopePartlySigned" xml:space="preserve">
<value>Teil-Signiert</value> <value>Teil-Signiert</value>
</data> </data>
@@ -180,6 +201,9 @@
<data name="EnvelopeReportCreated" xml:space="preserve"> <data name="EnvelopeReportCreated" xml:space="preserve">
<value>Signierungszertifikat erstellt</value> <value>Signierungszertifikat erstellt</value>
</data> </data>
<data name="EnvelopeReportCreatedRaC" xml:space="preserve">
<value>Lesebestätigungszertifikat erstellt</value>
</data>
<data name="EnvelopeSaved" xml:space="preserve"> <data name="EnvelopeSaved" xml:space="preserve">
<value>Gespeichert</value> <value>Gespeichert</value>
</data> </data>
@@ -198,6 +222,9 @@
<data name="MessageConfirmationSent" xml:space="preserve"> <data name="MessageConfirmationSent" xml:space="preserve">
<value>Signaturbestätigung versendet</value> <value>Signaturbestätigung versendet</value>
</data> </data>
<data name="MessageConfirmationSentRaC" xml:space="preserve">
<value>Lesebestätigung versendet</value>
</data>
<data name="MessageDeletionSent" xml:space="preserve"> <data name="MessageDeletionSent" xml:space="preserve">
<value>Löschinformation versendet</value> <value>Löschinformation versendet</value>
</data> </data>
@@ -207,6 +234,9 @@
<data name="No" xml:space="preserve"> <data name="No" xml:space="preserve">
<value>Nein</value> <value>Nein</value>
</data> </data>
<data name="PartlyConfirmed" xml:space="preserve">
<value>Teil-Bestätigt</value>
</data>
<data name="PartlySigned" xml:space="preserve"> <data name="PartlySigned" xml:space="preserve">
<value>Teil-Signiert</value> <value>Teil-Signiert</value>
</data> </data>
@@ -214,7 +244,7 @@
<value>Qualifizierte Signatur</value> <value>Qualifizierte Signatur</value>
</data> </data>
<data name="ReadAndSign" xml:space="preserve"> <data name="ReadAndSign" xml:space="preserve">
<value>Arbeitsanweisung</value> <value>Lesebestätigung</value>
</data> </data>
<data name="ResetTOTPUser" xml:space="preserve"> <data name="ResetTOTPUser" xml:space="preserve">
<value>Wollen Sie die 2-Faktor Definition für diesen Empfänger zurücksetzen. Der Empfänger muss sich dann neu identifizieren!</value> <value>Wollen Sie die 2-Faktor Definition für diesen Empfänger zurücksetzen. Der Empfänger muss sich dann neu identifizieren!</value>
@@ -229,7 +259,7 @@
<value>Signatur</value> <value>Signatur</value>
</data> </data>
<data name="SignatureConfirmed" xml:space="preserve"> <data name="SignatureConfirmed" xml:space="preserve">
<value>Signatur bestätigt</value> <value>Abschluss bestätigt</value>
</data> </data>
<data name="Signed" xml:space="preserve"> <data name="Signed" xml:space="preserve">
<value>Signiert</value> <value>Signiert</value>

View File

@@ -35,6 +35,9 @@ namespace EnvelopeGenerator.Domain.Entities
[Column("STATUS")] [Column("STATUS")]
public Constants.DocumentStatus Status { get; set; } public Constants.DocumentStatus Status { get; set; }
[Column("STATUS_CHANGED_WHEN", TypeName = "datetime")]
public DateTime? StatusChangedWhen { get; set; }
[Required] [Required]
[Column("ADDED_WHEN", TypeName = "datetime")] [Column("ADDED_WHEN", TypeName = "datetime")]
public DateTime AddedWhen { get; set; } public DateTime AddedWhen { get; set; }

View File

@@ -5,6 +5,7 @@ using System.ComponentModel.DataAnnotations.Schema;
using EnvelopeGenerator.Domain.Constants; using EnvelopeGenerator.Domain.Constants;
using Newtonsoft.Json; using Newtonsoft.Json;
using EnvelopeGenerator.Domain.Interfaces.Auditing; using EnvelopeGenerator.Domain.Interfaces.Auditing;
using EnvelopeGenerator.Domain.Interfaces;
#if NETFRAMEWORK #if NETFRAMEWORK
using System.Collections.Generic; using System.Collections.Generic;
using System.Linq; using System.Linq;
@@ -13,7 +14,7 @@ using System.Linq;
namespace EnvelopeGenerator.Domain.Entities namespace EnvelopeGenerator.Domain.Entities
{ {
[Table("TBSIG_ENVELOPE", Schema = "dbo")] [Table("TBSIG_ENVELOPE", Schema = "dbo")]
public class Envelope : IHasAddedWhen, IHasChangedWhen public class Envelope : IHasAddedWhen, IHasChangedWhen, IEnvelope
{ {
public Envelope() public Envelope()
{ {
@@ -106,7 +107,8 @@ namespace EnvelopeGenerator.Domain.Entities
[JsonIgnore] [JsonIgnore]
[NotMapped] [NotMapped]
public bool ReadOnly => EnvelopeTypeId == 2; [Obsolete("Use EnvelopeGenerator.Domain.Interfaces.EnvelopeExtensions.IsReadAndConfirm extension method instead.")]
public bool ReadOnly => this.IsReadAndConfirm();
[Column("CERTIFICATION_TYPE")] [Column("CERTIFICATION_TYPE")]
public int? CertificationType { get; set; } public int? CertificationType { get; set; }

View File

@@ -35,7 +35,10 @@ namespace EnvelopeGenerator.Domain.Entities
public DateTime AddedWhen { get; set; } public DateTime AddedWhen { get; set; }
[Column("ACTION_DATE", TypeName = "datetime")] [Column("ACTION_DATE", TypeName = "datetime")]
public DateTime? ChangedWhen { get; set; } public DateTime? ActionDate { get; set; }
[NotMapped]
public DateTime? ChangedWhen { get => ActionDate; set => ActionDate = value; }
[Column("COMMENT", TypeName = "nvarchar(max)")] [Column("COMMENT", TypeName = "nvarchar(max)")]
public string public string

View File

@@ -0,0 +1,20 @@
namespace EnvelopeGenerator.Domain.Interfaces
{
public interface IEnvelope
{
int? EnvelopeTypeId { get; set; }
}
public static class EnvelopeExtensions
{
public static bool IsReadAndConfirm(this IEnvelope envelope)
{
return envelope.EnvelopeTypeId == 2;
}
public static bool IsReadAndSign(this IEnvelope envelope)
{
return envelope.EnvelopeTypeId != 2;
}
}
}

View File

@@ -1,10 +1,10 @@
//------------------------------------------------------------------------------ //------------------------------------------------------------------------------
// <auto-generated> // <auto-generated>
// Dieser Code wurde von einem Tool generiert. // This code was generated by a tool.
// Laufzeitversion:4.0.30319.42000 // Runtime Version:4.0.30319.42000
// //
// Änderungen an dieser Datei können falsches Verhalten verursachen und gehen verloren, wenn // Changes to this file may cause incorrect behavior and will be lost if
// der Code erneut generiert wird. // the code is regenerated.
// </auto-generated> // </auto-generated>
//------------------------------------------------------------------------------ //------------------------------------------------------------------------------
@@ -13,12 +13,12 @@ namespace My.Resources {
/// <summary> /// <summary>
/// Eine stark typisierte Ressourcenklasse zum Suchen von lokalisierten Zeichenfolgen usw. /// A strongly-typed resource class, for looking up localized strings, etc.
/// </summary> /// </summary>
// Diese Klasse wurde von der StronglyTypedResourceBuilder automatisch generiert // This class was auto-generated by the StronglyTypedResourceBuilder
// -Klasse über ein Tool wie ResGen oder Visual Studio automatisch generiert. // class via a tool like ResGen or Visual Studio.
// Um einen Member hinzuzufügen oder zu entfernen, bearbeiten Sie die .ResX-Datei und führen dann ResGen // To add or remove a member, edit your .ResX file then rerun ResGen
// mit der /str-Option erneut aus, oder Sie erstellen Ihr VS-Projekt neu. // with the /str option, or rebuild your VS project.
[global::System.CodeDom.Compiler.GeneratedCodeAttribute("System.Resources.Tools.StronglyTypedResourceBuilder", "17.0.0.0")] [global::System.CodeDom.Compiler.GeneratedCodeAttribute("System.Resources.Tools.StronglyTypedResourceBuilder", "17.0.0.0")]
[global::System.Diagnostics.DebuggerNonUserCodeAttribute()] [global::System.Diagnostics.DebuggerNonUserCodeAttribute()]
[global::System.Runtime.CompilerServices.CompilerGeneratedAttribute()] [global::System.Runtime.CompilerServices.CompilerGeneratedAttribute()]
@@ -33,7 +33,7 @@ namespace My.Resources {
} }
/// <summary> /// <summary>
/// Gibt die zwischengespeicherte ResourceManager-Instanz zurück, die von dieser Klasse verwendet wird. /// Returns the cached ResourceManager instance used by this class.
/// </summary> /// </summary>
[global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)] [global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)]
public static global::System.Resources.ResourceManager ResourceManager { public static global::System.Resources.ResourceManager ResourceManager {
@@ -47,8 +47,8 @@ namespace My.Resources {
} }
/// <summary> /// <summary>
/// Überschreibt die CurrentUICulture-Eigenschaft des aktuellen Threads für alle /// Overrides the current thread's CurrentUICulture property for all
/// Ressourcenzuordnungen, die diese stark typisierte Ressourcenklasse verwenden. /// resource lookups using this strongly typed resource class.
/// </summary> /// </summary>
[global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)] [global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)]
public static global::System.Globalization.CultureInfo Culture { public static global::System.Globalization.CultureInfo Culture {
@@ -61,7 +61,7 @@ namespace My.Resources {
} }
/// <summary> /// <summary>
/// Sucht eine lokalisierte Zeichenfolge, die Zugriffscode korrekt eingegeben ähnelt. /// Looks up a localized string similar to Zugriffscode korrekt eingegeben.
/// </summary> /// </summary>
public static string AccessCodeCorrect { public static string AccessCodeCorrect {
get { get {
@@ -70,7 +70,7 @@ namespace My.Resources {
} }
/// <summary> /// <summary>
/// Sucht eine lokalisierte Zeichenfolge, die Zugriffscode falsch eingegeben ähnelt. /// Looks up a localized string similar to Zugriffscode falsch eingegeben.
/// </summary> /// </summary>
public static string AccessCodeIncorrect { public static string AccessCodeIncorrect {
get { get {
@@ -79,7 +79,7 @@ namespace My.Resources {
} }
/// <summary> /// <summary>
/// Sucht eine lokalisierte Zeichenfolge, die Zugriffscode angefordert ähnelt. /// Looks up a localized string similar to Zugriffscode angefordert.
/// </summary> /// </summary>
public static string AccessCodeRequested { public static string AccessCodeRequested {
get { get {
@@ -88,7 +88,7 @@ namespace My.Resources {
} }
/// <summary> /// <summary>
/// Sucht eine lokalisierte Zeichenfolge, die Fortgeschrittene Elektronische Signatur ähnelt. /// Looks up a localized string similar to Fortgeschrittene Elektronische Signatur.
/// </summary> /// </summary>
public static string AdvancedElectronicSignature { public static string AdvancedElectronicSignature {
get { get {
@@ -97,7 +97,7 @@ namespace My.Resources {
} }
/// <summary> /// <summary>
/// Sucht eine lokalisierte Zeichenfolge, die Abgeschlossen ähnelt. /// Looks up a localized string similar to Abgeschlossen.
/// </summary> /// </summary>
public static string Completed { public static string Completed {
get { get {
@@ -106,7 +106,16 @@ namespace My.Resources {
} }
/// <summary> /// <summary>
/// Sucht eine lokalisierte Zeichenfolge, die Vollständig Signiert ähnelt. /// Looks up a localized string similar to Vollständig bestätigt.
/// </summary>
public static string CompletelyConfirmed {
get {
return ResourceManager.GetString("CompletelyConfirmed", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to Vollständig signiert.
/// </summary> /// </summary>
public static string CompletelySigned { public static string CompletelySigned {
get { get {
@@ -115,7 +124,25 @@ namespace My.Resources {
} }
/// <summary> /// <summary>
/// Sucht eine lokalisierte Zeichenfolge, die Vertrag ähnelt. /// Looks up a localized string similar to Lesebestätigung.
/// </summary>
public static string Confirmation {
get {
return ResourceManager.GetString("Confirmation", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to Gelesen und bestätigt.
/// </summary>
public static string Confirmed {
get {
return ResourceManager.GetString("Confirmed", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to Vertrag.
/// </summary> /// </summary>
public static string Contract { public static string Contract {
get { get {
@@ -124,7 +151,7 @@ namespace My.Resources {
} }
/// <summary> /// <summary>
/// Sucht eine lokalisierte Zeichenfolge, die Erstellt ähnelt. /// Looks up a localized string similar to Erstellt.
/// </summary> /// </summary>
public static string Created { public static string Created {
get { get {
@@ -133,7 +160,16 @@ namespace My.Resources {
} }
/// <summary> /// <summary>
/// Sucht eine lokalisierte Zeichenfolge, die Dokument Rotation geändert ähnelt. /// Looks up a localized string similar to Dokument gelesen und bestätigt.
/// </summary>
public static string DocumentConfirmed {
get {
return ResourceManager.GetString("DocumentConfirmed", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to Dokument Rotation geändert.
/// </summary> /// </summary>
public static string DocumentMod_Rotation { public static string DocumentMod_Rotation {
get { get {
@@ -142,7 +178,7 @@ namespace My.Resources {
} }
/// <summary> /// <summary>
/// Sucht eine lokalisierte Zeichenfolge, die Dokument geöffnet ähnelt. /// Looks up a localized string similar to Dokument geöffnet.
/// </summary> /// </summary>
public static string DocumentOpened { public static string DocumentOpened {
get { get {
@@ -151,7 +187,7 @@ namespace My.Resources {
} }
/// <summary> /// <summary>
/// Sucht eine lokalisierte Zeichenfolge, die Unterzeichnung abgelehnt ähnelt. /// Looks up a localized string similar to Unterzeichnung abgelehnt.
/// </summary> /// </summary>
public static string DocumentRejected { public static string DocumentRejected {
get { get {
@@ -160,7 +196,16 @@ namespace My.Resources {
} }
/// <summary> /// <summary>
/// Sucht eine lokalisierte Zeichenfolge, die Dokument unterzeichnet ähnelt. /// Looks up a localized string similar to Lesebestätigung abgelehnt.
/// </summary>
public static string DocumentRejectedRaC {
get {
return ResourceManager.GetString("DocumentRejectedRaC", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to Dokument unterzeichnet.
/// </summary> /// </summary>
public static string DocumentSigned { public static string DocumentSigned {
get { get {
@@ -169,7 +214,7 @@ namespace My.Resources {
} }
/// <summary> /// <summary>
/// Sucht eine lokalisierte Zeichenfolge, die Entwurf ähnelt. /// Looks up a localized string similar to Entwurf.
/// </summary> /// </summary>
public static string Draft { public static string Draft {
get { get {
@@ -178,7 +223,7 @@ namespace My.Resources {
} }
/// <summary> /// <summary>
/// Sucht eine lokalisierte Zeichenfolge, die Archiviert ähnelt. /// Looks up a localized string similar to Archiviert.
/// </summary> /// </summary>
public static string EnvelopeArchived { public static string EnvelopeArchived {
get { get {
@@ -187,7 +232,16 @@ namespace My.Resources {
} }
/// <summary> /// <summary>
/// Sucht eine lokalisierte Zeichenfolge, die Vollständig signiert ähnelt. /// Looks up a localized string similar to Vollständig gelesen und bestätigt.
/// </summary>
public static string EnvelopeCompletelyConfirmed {
get {
return ResourceManager.GetString("EnvelopeCompletelyConfirmed", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to Vollständig signiert.
/// </summary> /// </summary>
public static string EnvelopeCompletelySigned { public static string EnvelopeCompletelySigned {
get { get {
@@ -196,7 +250,7 @@ namespace My.Resources {
} }
/// <summary> /// <summary>
/// Sucht eine lokalisierte Zeichenfolge, die Umschlag Erstellt ähnelt. /// Looks up a localized string similar to Umschlag Erstellt.
/// </summary> /// </summary>
public static string EnvelopeCreated { public static string EnvelopeCreated {
get { get {
@@ -205,7 +259,7 @@ namespace My.Resources {
} }
/// <summary> /// <summary>
/// Sucht eine lokalisierte Zeichenfolge, die Umschlag Gelöscht ähnelt. /// Looks up a localized string similar to Umschlag Gelöscht.
/// </summary> /// </summary>
public static string EnvelopeDeleted { public static string EnvelopeDeleted {
get { get {
@@ -214,7 +268,16 @@ namespace My.Resources {
} }
/// <summary> /// <summary>
/// Sucht eine lokalisierte Zeichenfolge, die Teil-Signiert ähnelt. /// Looks up a localized string similar to Teil-Bestätigt.
/// </summary>
public static string EnvelopePartlyConfirmed {
get {
return ResourceManager.GetString("EnvelopePartlyConfirmed", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to Teil-Signiert.
/// </summary> /// </summary>
public static string EnvelopePartlySigned { public static string EnvelopePartlySigned {
get { get {
@@ -223,7 +286,7 @@ namespace My.Resources {
} }
/// <summary> /// <summary>
/// Sucht eine lokalisierte Zeichenfolge, die Umschlag in Queue ähnelt. /// Looks up a localized string similar to Umschlag in Queue.
/// </summary> /// </summary>
public static string EnvelopeQueued { public static string EnvelopeQueued {
get { get {
@@ -232,7 +295,7 @@ namespace My.Resources {
} }
/// <summary> /// <summary>
/// Sucht eine lokalisierte Zeichenfolge, die Umschlag abgelehnt ähnelt. /// Looks up a localized string similar to Umschlag abgelehnt.
/// </summary> /// </summary>
public static string EnvelopeRejected { public static string EnvelopeRejected {
get { get {
@@ -241,7 +304,7 @@ namespace My.Resources {
} }
/// <summary> /// <summary>
/// Sucht eine lokalisierte Zeichenfolge, die Signierungszertifikat erstellt ähnelt. /// Looks up a localized string similar to Signierungszertifikat erstellt.
/// </summary> /// </summary>
public static string EnvelopeReportCreated { public static string EnvelopeReportCreated {
get { get {
@@ -250,7 +313,16 @@ namespace My.Resources {
} }
/// <summary> /// <summary>
/// Sucht eine lokalisierte Zeichenfolge, die Gespeichert ähnelt. /// Looks up a localized string similar to Lesebestätigungszertifikat erstellt.
/// </summary>
public static string EnvelopeReportCreatedRaC {
get {
return ResourceManager.GetString("EnvelopeReportCreatedRaC", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to Gespeichert.
/// </summary> /// </summary>
public static string EnvelopeSaved { public static string EnvelopeSaved {
get { get {
@@ -259,7 +331,7 @@ namespace My.Resources {
} }
/// <summary> /// <summary>
/// Sucht eine lokalisierte Zeichenfolge, die Gesendet ähnelt. /// Looks up a localized string similar to Gesendet.
/// </summary> /// </summary>
public static string EnvelopeSent { public static string EnvelopeSent {
get { get {
@@ -268,7 +340,7 @@ namespace My.Resources {
} }
/// <summary> /// <summary>
/// Sucht eine lokalisierte Zeichenfolge, die Umschlag zurückgezogen ähnelt. /// Looks up a localized string similar to Umschlag zurückgezogen.
/// </summary> /// </summary>
public static string EnvelopeWithdrawn { public static string EnvelopeWithdrawn {
get { get {
@@ -277,7 +349,7 @@ namespace My.Resources {
} }
/// <summary> /// <summary>
/// Sucht eine lokalisierte Zeichenfolge, die Zugriffscode versendet ähnelt. /// Looks up a localized string similar to Zugriffscode versendet.
/// </summary> /// </summary>
public static string MessageAccessCodeSent { public static string MessageAccessCodeSent {
get { get {
@@ -286,7 +358,7 @@ namespace My.Resources {
} }
/// <summary> /// <summary>
/// Sucht eine lokalisierte Zeichenfolge, die Abschlussemail versendet ähnelt. /// Looks up a localized string similar to Abschlussemail versendet.
/// </summary> /// </summary>
public static string MessageCompletionSent { public static string MessageCompletionSent {
get { get {
@@ -295,7 +367,7 @@ namespace My.Resources {
} }
/// <summary> /// <summary>
/// Sucht eine lokalisierte Zeichenfolge, die Signaturbestätigung versendet ähnelt. /// Looks up a localized string similar to Abschlussbestätigung versendet.
/// </summary> /// </summary>
public static string MessageConfirmationSent { public static string MessageConfirmationSent {
get { get {
@@ -304,7 +376,7 @@ namespace My.Resources {
} }
/// <summary> /// <summary>
/// Sucht eine lokalisierte Zeichenfolge, die Löschinformation versendet ähnelt. /// Looks up a localized string similar to Löschinformation versendet.
/// </summary> /// </summary>
public static string MessageDeletionSent { public static string MessageDeletionSent {
get { get {
@@ -313,7 +385,7 @@ namespace My.Resources {
} }
/// <summary> /// <summary>
/// Sucht eine lokalisierte Zeichenfolge, die Dokumentenlink versendet ähnelt. /// Looks up a localized string similar to Dokumentenlink versendet.
/// </summary> /// </summary>
public static string MessageInvitationSent { public static string MessageInvitationSent {
get { get {
@@ -322,7 +394,7 @@ namespace My.Resources {
} }
/// <summary> /// <summary>
/// Sucht eine lokalisierte Zeichenfolge, die Nein ähnelt. /// Looks up a localized string similar to Nein.
/// </summary> /// </summary>
public static string No { public static string No {
get { get {
@@ -331,7 +403,16 @@ namespace My.Resources {
} }
/// <summary> /// <summary>
/// Sucht eine lokalisierte Zeichenfolge, die Teil-Signiert ähnelt. /// Looks up a localized string similar to Teil-Bestätigt.
/// </summary>
public static string PartlyConfirmed {
get {
return ResourceManager.GetString("PartlyConfirmed", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to Teil-Signiert.
/// </summary> /// </summary>
public static string PartlySigned { public static string PartlySigned {
get { get {
@@ -340,7 +421,7 @@ namespace My.Resources {
} }
/// <summary> /// <summary>
/// Sucht eine lokalisierte Zeichenfolge, die Qualifizierte Signatur ähnelt. /// Looks up a localized string similar to Qualifizierte Signatur.
/// </summary> /// </summary>
public static string QualifiedSignature { public static string QualifiedSignature {
get { get {
@@ -349,7 +430,7 @@ namespace My.Resources {
} }
/// <summary> /// <summary>
/// Sucht eine lokalisierte Zeichenfolge, die Arbeitsanweisung ähnelt. /// Looks up a localized string similar to Lesebestätigung.
/// </summary> /// </summary>
public static string ReadAndSign { public static string ReadAndSign {
get { get {
@@ -358,7 +439,7 @@ namespace My.Resources {
} }
/// <summary> /// <summary>
/// Sucht eine lokalisierte Zeichenfolge, die Wollen Sie die 2-Faktor Definition für diesen Empfänger zurücksetzen. Der Empfänger muss sich dann neu identifizieren! ähnelt. /// Looks up a localized string similar to Wollen Sie die 2-Faktor Definition für diesen Empfänger zurücksetzen. Der Empfänger muss sich dann neu identifizieren!.
/// </summary> /// </summary>
public static string ResetTOTPUser { public static string ResetTOTPUser {
get { get {
@@ -367,7 +448,7 @@ namespace My.Resources {
} }
/// <summary> /// <summary>
/// Sucht eine lokalisierte Zeichenfolge, die Gespeichert ähnelt. /// Looks up a localized string similar to Gespeichert.
/// </summary> /// </summary>
public static string Saved { public static string Saved {
get { get {
@@ -376,7 +457,7 @@ namespace My.Resources {
} }
/// <summary> /// <summary>
/// Sucht eine lokalisierte Zeichenfolge, die Gesendet ähnelt. /// Looks up a localized string similar to Gesendet.
/// </summary> /// </summary>
public static string Sent { public static string Sent {
get { get {
@@ -385,7 +466,7 @@ namespace My.Resources {
} }
/// <summary> /// <summary>
/// Sucht eine lokalisierte Zeichenfolge, die Signatur ähnelt. /// Looks up a localized string similar to Signatur.
/// </summary> /// </summary>
public static string Signature { public static string Signature {
get { get {
@@ -394,7 +475,7 @@ namespace My.Resources {
} }
/// <summary> /// <summary>
/// Sucht eine lokalisierte Zeichenfolge, die Signatur bestätigt ähnelt. /// Looks up a localized string similar to Abschluss bestätigt.
/// </summary> /// </summary>
public static string SignatureConfirmed { public static string SignatureConfirmed {
get { get {
@@ -403,7 +484,7 @@ namespace My.Resources {
} }
/// <summary> /// <summary>
/// Sucht eine lokalisierte Zeichenfolge, die Signiert ähnelt. /// Looks up a localized string similar to Signiert.
/// </summary> /// </summary>
public static string Signed { public static string Signed {
get { get {
@@ -412,7 +493,7 @@ namespace My.Resources {
} }
/// <summary> /// <summary>
/// Sucht eine lokalisierte Zeichenfolge, die Erfolgreich! Dialog wird geschlossen. ähnelt. /// Looks up a localized string similar to Erfolgreich! Dialog wird geschlossen..
/// </summary> /// </summary>
public static string Success_FormClose { public static string Success_FormClose {
get { get {
@@ -421,7 +502,16 @@ namespace My.Resources {
} }
/// <summary> /// <summary>
/// Sucht eine lokalisierte Zeichenfolge, die Unsigniert ähnelt. /// Looks up a localized string similar to Unbestätigt.
/// </summary>
public static string Unconfirmed {
get {
return ResourceManager.GetString("Unconfirmed", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to Unsigniert.
/// </summary> /// </summary>
public static string Unsigned { public static string Unsigned {
get { get {
@@ -430,7 +520,7 @@ namespace My.Resources {
} }
/// <summary> /// <summary>
/// Sucht eine lokalisierte Zeichenfolge, die Ja ähnelt. /// Looks up a localized string similar to Ja.
/// </summary> /// </summary>
public static string Yes { public static string Yes {
get { get {
@@ -439,7 +529,7 @@ namespace My.Resources {
} }
/// <summary> /// <summary>
/// Sucht eine lokalisierte Zeichenfolge, die Ja, mit Anhang ähnelt. /// Looks up a localized string similar to Ja, mit Anhang.
/// </summary> /// </summary>
public static string YesWithAttachment { public static string YesWithAttachment {
get { get {

View File

@@ -132,15 +132,27 @@
<data name="Completed" xml:space="preserve"> <data name="Completed" xml:space="preserve">
<value>Completed</value> <value>Completed</value>
</data> </data>
<data name="CompletelyConfirmed" xml:space="preserve">
<value>Completely confirmed</value>
</data>
<data name="CompletelySigned" xml:space="preserve"> <data name="CompletelySigned" xml:space="preserve">
<value>Completely signed</value> <value>Completely signed</value>
</data> </data>
<data name="Confirmation" xml:space="preserve">
<value>Read Confirmation</value>
</data>
<data name="Confirmed" xml:space="preserve">
<value>Read and signed</value>
</data>
<data name="Contract" xml:space="preserve"> <data name="Contract" xml:space="preserve">
<value>Contract</value> <value>Contract</value>
</data> </data>
<data name="Created" xml:space="preserve"> <data name="Created" xml:space="preserve">
<value>Created</value> <value>Created</value>
</data> </data>
<data name="DocumentConfirmed" xml:space="preserve">
<value>Document read and signed</value>
</data>
<data name="DocumentMod_Rotation" xml:space="preserve"> <data name="DocumentMod_Rotation" xml:space="preserve">
<value>Document rotation adapted</value> <value>Document rotation adapted</value>
</data> </data>
@@ -150,6 +162,9 @@
<data name="DocumentRejected" xml:space="preserve"> <data name="DocumentRejected" xml:space="preserve">
<value>Signing rejected</value> <value>Signing rejected</value>
</data> </data>
<data name="DocumentRejectedRaC" xml:space="preserve">
<value>Read confirmation rejected</value>
</data>
<data name="DocumentSigned" xml:space="preserve"> <data name="DocumentSigned" xml:space="preserve">
<value>Document signed</value> <value>Document signed</value>
</data> </data>
@@ -159,6 +174,9 @@
<data name="EnvelopeArchived" xml:space="preserve"> <data name="EnvelopeArchived" xml:space="preserve">
<value>Archived</value> <value>Archived</value>
</data> </data>
<data name="EnvelopeCompletelyConfirmed" xml:space="preserve">
<value>Completely confirmed</value>
</data>
<data name="EnvelopeCompletelySigned" xml:space="preserve"> <data name="EnvelopeCompletelySigned" xml:space="preserve">
<value>Completely signed</value> <value>Completely signed</value>
</data> </data>
@@ -168,8 +186,11 @@
<data name="EnvelopeDeleted" xml:space="preserve"> <data name="EnvelopeDeleted" xml:space="preserve">
<value>Envelope Deleted</value> <value>Envelope Deleted</value>
</data> </data>
<data name="EnvelopePartlyConfirmed" xml:space="preserve">
<value>Partially confirmed</value>
</data>
<data name="EnvelopePartlySigned" xml:space="preserve"> <data name="EnvelopePartlySigned" xml:space="preserve">
<value>Partly signed</value> <value>Partially signed</value>
</data> </data>
<data name="EnvelopeQueued" xml:space="preserve"> <data name="EnvelopeQueued" xml:space="preserve">
<value>Envelope Queued</value> <value>Envelope Queued</value>
@@ -180,6 +201,9 @@
<data name="EnvelopeReportCreated" xml:space="preserve"> <data name="EnvelopeReportCreated" xml:space="preserve">
<value>Signature certificate created</value> <value>Signature certificate created</value>
</data> </data>
<data name="EnvelopeReportCreatedRaC" xml:space="preserve">
<value>Read confirmartion certificate created</value>
</data>
<data name="EnvelopeSaved" xml:space="preserve"> <data name="EnvelopeSaved" xml:space="preserve">
<value>Saved</value> <value>Saved</value>
</data> </data>
@@ -196,7 +220,7 @@
<value>Final email sent</value> <value>Final email sent</value>
</data> </data>
<data name="MessageConfirmationSent" xml:space="preserve"> <data name="MessageConfirmationSent" xml:space="preserve">
<value>Confirmation Sent</value> <value>Finalization Confirmation Sent</value>
</data> </data>
<data name="MessageDeletionSent" xml:space="preserve"> <data name="MessageDeletionSent" xml:space="preserve">
<value>Deletion Notice Sent</value> <value>Deletion Notice Sent</value>
@@ -207,6 +231,12 @@
<data name="No" xml:space="preserve"> <data name="No" xml:space="preserve">
<value>No</value> <value>No</value>
</data> </data>
<data name="PartlyConfirmed" xml:space="preserve">
<value>Partially confirmed</value>
</data>
<data name="PartlySigned" xml:space="preserve">
<value>Partially signed</value>
</data>
<data name="QualifiedSignature" xml:space="preserve"> <data name="QualifiedSignature" xml:space="preserve">
<value>Qualified Signature</value> <value>Qualified Signature</value>
</data> </data>
@@ -226,7 +256,7 @@
<value>Signature</value> <value>Signature</value>
</data> </data>
<data name="SignatureConfirmed" xml:space="preserve"> <data name="SignatureConfirmed" xml:space="preserve">
<value>Signature confirmed</value> <value>Finalization confirmed</value>
</data> </data>
<data name="Signed" xml:space="preserve"> <data name="Signed" xml:space="preserve">
<value>Signed</value> <value>Signed</value>
@@ -234,6 +264,9 @@
<data name="Success_FormClose" xml:space="preserve"> <data name="Success_FormClose" xml:space="preserve">
<value>Successful! Dialog is closed.successful! Dialog is closed.</value> <value>Successful! Dialog is closed.successful! Dialog is closed.</value>
</data> </data>
<data name="Unconfirmed" xml:space="preserve">
<value>Unconfirmed</value>
</data>
<data name="Unsigned" xml:space="preserve"> <data name="Unsigned" xml:space="preserve">
<value>Unsigned</value> <value>Unsigned</value>
</data> </data>

View File

@@ -132,8 +132,17 @@
<data name="Completed" xml:space="preserve"> <data name="Completed" xml:space="preserve">
<value>Abgeschlossen</value> <value>Abgeschlossen</value>
</data> </data>
<data name="CompletelyConfirmed" xml:space="preserve">
<value>Vollständig bestätigt</value>
</data>
<data name="CompletelySigned" xml:space="preserve"> <data name="CompletelySigned" xml:space="preserve">
<value>Vollständig Signiert</value> <value>Vollständig signiert</value>
</data>
<data name="Confirmation" xml:space="preserve">
<value>Lesebestätigung</value>
</data>
<data name="Confirmed" xml:space="preserve">
<value>Gelesen und bestätigt</value>
</data> </data>
<data name="Contract" xml:space="preserve"> <data name="Contract" xml:space="preserve">
<value>Vertrag</value> <value>Vertrag</value>
@@ -141,6 +150,9 @@
<data name="Created" xml:space="preserve"> <data name="Created" xml:space="preserve">
<value>Erstellt</value> <value>Erstellt</value>
</data> </data>
<data name="DocumentConfirmed" xml:space="preserve">
<value>Dokument gelesen und bestätigt</value>
</data>
<data name="DocumentMod_Rotation" xml:space="preserve"> <data name="DocumentMod_Rotation" xml:space="preserve">
<value>Dokument Rotation geändert</value> <value>Dokument Rotation geändert</value>
</data> </data>
@@ -150,6 +162,9 @@
<data name="DocumentRejected" xml:space="preserve"> <data name="DocumentRejected" xml:space="preserve">
<value>Unterzeichnung abgelehnt</value> <value>Unterzeichnung abgelehnt</value>
</data> </data>
<data name="DocumentRejectedRaC" xml:space="preserve">
<value>Lesebestätigung abgelehnt</value>
</data>
<data name="DocumentSigned" xml:space="preserve"> <data name="DocumentSigned" xml:space="preserve">
<value>Dokument unterzeichnet</value> <value>Dokument unterzeichnet</value>
</data> </data>
@@ -159,6 +174,9 @@
<data name="EnvelopeArchived" xml:space="preserve"> <data name="EnvelopeArchived" xml:space="preserve">
<value>Archiviert</value> <value>Archiviert</value>
</data> </data>
<data name="EnvelopeCompletelyConfirmed" xml:space="preserve">
<value>Vollständig gelesen und bestätigt</value>
</data>
<data name="EnvelopeCompletelySigned" xml:space="preserve"> <data name="EnvelopeCompletelySigned" xml:space="preserve">
<value>Vollständig signiert</value> <value>Vollständig signiert</value>
</data> </data>
@@ -168,6 +186,9 @@
<data name="EnvelopeDeleted" xml:space="preserve"> <data name="EnvelopeDeleted" xml:space="preserve">
<value>Umschlag Gelöscht</value> <value>Umschlag Gelöscht</value>
</data> </data>
<data name="EnvelopePartlyConfirmed" xml:space="preserve">
<value>Teil-Bestätigt</value>
</data>
<data name="EnvelopePartlySigned" xml:space="preserve"> <data name="EnvelopePartlySigned" xml:space="preserve">
<value>Teil-Signiert</value> <value>Teil-Signiert</value>
</data> </data>
@@ -180,6 +201,9 @@
<data name="EnvelopeReportCreated" xml:space="preserve"> <data name="EnvelopeReportCreated" xml:space="preserve">
<value>Signierungszertifikat erstellt</value> <value>Signierungszertifikat erstellt</value>
</data> </data>
<data name="EnvelopeReportCreatedRaC" xml:space="preserve">
<value>Lesebestätigungszertifikat erstellt</value>
</data>
<data name="EnvelopeSaved" xml:space="preserve"> <data name="EnvelopeSaved" xml:space="preserve">
<value>Gespeichert</value> <value>Gespeichert</value>
</data> </data>
@@ -196,7 +220,7 @@
<value>Abschlussemail versendet</value> <value>Abschlussemail versendet</value>
</data> </data>
<data name="MessageConfirmationSent" xml:space="preserve"> <data name="MessageConfirmationSent" xml:space="preserve">
<value>Signaturbestätigung versendet</value> <value>Abschlussbestätigung versendet</value>
</data> </data>
<data name="MessageDeletionSent" xml:space="preserve"> <data name="MessageDeletionSent" xml:space="preserve">
<value>Löschinformation versendet</value> <value>Löschinformation versendet</value>
@@ -207,6 +231,9 @@
<data name="No" xml:space="preserve"> <data name="No" xml:space="preserve">
<value>Nein</value> <value>Nein</value>
</data> </data>
<data name="PartlyConfirmed" xml:space="preserve">
<value>Teil-Bestätigt</value>
</data>
<data name="PartlySigned" xml:space="preserve"> <data name="PartlySigned" xml:space="preserve">
<value>Teil-Signiert</value> <value>Teil-Signiert</value>
</data> </data>
@@ -214,7 +241,7 @@
<value>Qualifizierte Signatur</value> <value>Qualifizierte Signatur</value>
</data> </data>
<data name="ReadAndSign" xml:space="preserve"> <data name="ReadAndSign" xml:space="preserve">
<value>Arbeitsanweisung</value> <value>Lesebestätigung</value>
</data> </data>
<data name="ResetTOTPUser" xml:space="preserve"> <data name="ResetTOTPUser" xml:space="preserve">
<value>Wollen Sie die 2-Faktor Definition für diesen Empfänger zurücksetzen. Der Empfänger muss sich dann neu identifizieren!</value> <value>Wollen Sie die 2-Faktor Definition für diesen Empfänger zurücksetzen. Der Empfänger muss sich dann neu identifizieren!</value>
@@ -229,7 +256,7 @@
<value>Signatur</value> <value>Signatur</value>
</data> </data>
<data name="SignatureConfirmed" xml:space="preserve"> <data name="SignatureConfirmed" xml:space="preserve">
<value>Signatur bestätigt</value> <value>Abschluss bestätigt</value>
</data> </data>
<data name="Signed" xml:space="preserve"> <data name="Signed" xml:space="preserve">
<value>Signiert</value> <value>Signiert</value>
@@ -237,6 +264,9 @@
<data name="Success_FormClose" xml:space="preserve"> <data name="Success_FormClose" xml:space="preserve">
<value>Erfolgreich! Dialog wird geschlossen.</value> <value>Erfolgreich! Dialog wird geschlossen.</value>
</data> </data>
<data name="Unconfirmed" xml:space="preserve">
<value>Unbestätigt</value>
</data>
<data name="Unsigned" xml:space="preserve"> <data name="Unsigned" xml:space="preserve">
<value>Unsigniert</value> <value>Unsigniert</value>
</data> </data>

View File

@@ -25,7 +25,7 @@
<PackageReference Include="DigitalData.Core.Abstraction.Application" Version="1.6.0" /> <PackageReference Include="DigitalData.Core.Abstraction.Application" Version="1.6.0" />
<PackageReference Include="DigitalData.Core.Infrastructure" Version="2.6.1" /> <PackageReference Include="DigitalData.Core.Infrastructure" Version="2.6.1" />
<PackageReference Include="HtmlSanitizer" Version="9.0.892" /> <PackageReference Include="HtmlSanitizer" Version="9.0.892" />
<PackageReference Include="Microsoft.Data.SqlClient" Version="6.1.4" /> <PackageReference Include="Microsoft.Data.SqlClient" Version="5.2.2" />
<PackageReference Include="Microsoft.Identity.Client" Version="4.82.1" /> <PackageReference Include="Microsoft.Identity.Client" Version="4.82.1" />
<PackageReference Include="QuestPDF" Version="2025.7.1" /> <PackageReference Include="QuestPDF" Version="2025.7.1" />
<PackageReference Include="SixLabors.ImageSharp" Version="3.1.12" /> <PackageReference Include="SixLabors.ImageSharp" Version="3.1.12" />

View File

@@ -1,26 +0,0 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net8.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="BouncyCastle.Cryptography" Version="2.6.2" />
<PackageReference Include="HtmlSanitizer" Version="9.0.892" />
<PackageReference Include="Microsoft.Identity.Client" Version="4.82.1" />
<PackageReference Include="Quartz" Version="3.9.0" />
<PackageReference Include="Newtonsoft.Json" Version="13.0.3" />
<PackageReference Include="Microsoft.Extensions.Logging.Abstractions" Version="8.0.3" />
<PackageReference Include="Microsoft.Data.SqlClient" Version="6.1.4" />
<PackageReference Include="SixLabors.ImageSharp" Version="3.1.12" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\EnvelopeGenerator.Domain\EnvelopeGenerator.Domain.csproj" />
<ProjectReference Include="..\EnvelopeGenerator.Infrastructure\EnvelopeGenerator.Infrastructure.csproj" />
<ProjectReference Include="..\EnvelopeGenerator.PdfEditor\EnvelopeGenerator.PdfEditor.csproj" />
</ItemGroup>
</Project>

View File

@@ -1,151 +0,0 @@
using System.Collections.Generic;
using System.Data;
using System.Threading.Tasks;
using EnvelopeGenerator.Domain.Constants;
using Microsoft.Data.SqlClient;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Logging.Abstractions;
using Quartz;
namespace EnvelopeGenerator.Jobs.APIBackendJobs;
public class APIEnvelopeJob(ILogger<APIEnvelopeJob>? logger = null) : IJob
{
private readonly ILogger<APIEnvelopeJob> _logger = logger ?? NullLogger<APIEnvelopeJob>.Instance;
public async Task Execute(IJobExecutionContext context)
{
var jobId = context.JobDetail.Key.ToString();
_logger.LogDebug("API Envelopes - Starting job {JobId}", jobId);
try
{
var connectionString = context.MergedJobDataMap.GetString(Value.DATABASE);
if (string.IsNullOrWhiteSpace(connectionString))
{
_logger.LogWarning("API Envelopes - Connection string missing");
return;
}
await using var connection = new SqlConnection(connectionString);
await connection.OpenAsync(context.CancellationToken);
await ProcessInvitationsAsync(connection, context.CancellationToken);
await ProcessWithdrawnAsync(connection, context.CancellationToken);
_logger.LogDebug("API Envelopes - Completed job {JobId} successfully", jobId);
}
catch (System.Exception ex)
{
_logger.LogError(ex, "API Envelopes job failed");
}
finally
{
_logger.LogDebug("API Envelopes execution for {JobId} ended", jobId);
}
}
private async Task ProcessInvitationsAsync(SqlConnection connection, System.Threading.CancellationToken cancellationToken)
{
const string sql = "SELECT GUID FROM TBSIG_ENVELOPE WHERE SOURCE = 'API' AND STATUS = 1003 ORDER BY GUID";
var envelopeIds = new List<int>();
await using (var command = new SqlCommand(sql, connection))
await using (var reader = await command.ExecuteReaderAsync(cancellationToken))
{
while (await reader.ReadAsync(cancellationToken))
{
if (reader[0] is int id)
{
envelopeIds.Add(id);
}
}
}
if (envelopeIds.Count == 0)
{
_logger.LogDebug("SendInvMail - No envelopes found");
return;
}
_logger.LogInformation("SendInvMail - Found {Count} envelopes", envelopeIds.Count);
var total = envelopeIds.Count;
var current = 1;
foreach (var id in envelopeIds)
{
_logger.LogInformation("SendInvMail - Processing Envelope {EnvelopeId} ({Current}/{Total})", id, current, total);
try
{
// Placeholder for invitation email sending logic.
_logger.LogDebug("SendInvMail - Marking envelope {EnvelopeId} as queued", id);
const string updateSql = "UPDATE TBSIG_ENVELOPE SET CURRENT_WORK_APP = @App WHERE GUID = @Id";
await using var updateCommand = new SqlCommand(updateSql, connection);
updateCommand.Parameters.AddWithValue("@App", "signFLOW_API_EnvJob_InvMail");
updateCommand.Parameters.AddWithValue("@Id", id);
await updateCommand.ExecuteNonQueryAsync(cancellationToken);
}
catch (System.Exception ex)
{
_logger.LogWarning(ex, "SendInvMail - Unhandled exception while working envelope {EnvelopeId}", id);
}
current++;
_logger.LogInformation("SendInvMail - Envelope finalized");
}
}
private async Task ProcessWithdrawnAsync(SqlConnection connection, System.Threading.CancellationToken cancellationToken)
{
const string sql = @"SELECT ENV.GUID, REJ.COMMENT AS REJECTION_REASON FROM
(SELECT * FROM TBSIG_ENVELOPE WHERE STATUS = 1009 AND SOURCE = 'API') ENV INNER JOIN
(SELECT MAX(GUID) GUID, ENVELOPE_ID, MAX(ADDED_WHEN) ADDED_WHEN, MAX(ACTION_DATE) ACTION_DATE, COMMENT FROM TBSIG_ENVELOPE_HISTORY WHERE STATUS = 1009 GROUP BY ENVELOPE_ID, COMMENT ) REJ ON ENV.GUID = REJ.ENVELOPE_ID LEFT JOIN
(SELECT * FROM TBSIG_ENVELOPE_HISTORY WHERE STATUS = 3004 ) M_Send ON ENV.GUID = M_Send.ENVELOPE_ID
WHERE M_Send.GUID IS NULL";
var withdrawn = new List<(int EnvelopeId, string Reason)>();
await using (var command = new SqlCommand(sql, connection))
await using (var reader = await command.ExecuteReaderAsync(cancellationToken))
{
while (await reader.ReadAsync(cancellationToken))
{
var id = reader.GetInt32(0);
var reason = reader.IsDBNull(1) ? string.Empty : reader.GetString(1);
withdrawn.Add((id, reason));
}
}
if (withdrawn.Count == 0)
{
_logger.LogDebug("WithdrawnEnv - No envelopes found");
return;
}
_logger.LogInformation("WithdrawnEnv - Found {Count} envelopes", withdrawn.Count);
var total = withdrawn.Count;
var current = 1;
foreach (var (envelopeId, reason) in withdrawn)
{
_logger.LogInformation("WithdrawnEnv - Processing Envelope {EnvelopeId} ({Current}/{Total})", envelopeId, current, total);
try
{
// Log withdrawn mail trigger placeholder
const string insertHistory = "INSERT INTO TBSIG_ENVELOPE_HISTORY (ENVELOPE_ID, STATUS, USER_REFERENCE, ADDED_WHEN, ACTION_DATE, COMMENT) VALUES (@EnvelopeId, @Status, @UserReference, GETDATE(), GETDATE(), @Comment)";
await using var insertCommand = new SqlCommand(insertHistory, connection);
insertCommand.Parameters.AddWithValue("@EnvelopeId", envelopeId);
insertCommand.Parameters.AddWithValue("@Status", 3004);
insertCommand.Parameters.AddWithValue("@UserReference", "API");
insertCommand.Parameters.AddWithValue("@Comment", reason ?? string.Empty);
await insertCommand.ExecuteNonQueryAsync(cancellationToken);
}
catch (System.Exception ex)
{
_logger.LogWarning(ex, "WithdrawnEnv - Unhandled exception while working envelope {EnvelopeId}", envelopeId);
}
current++;
_logger.LogInformation("WithdrawnEnv - Envelope finalized");
}
}
}

View File

@@ -1,30 +0,0 @@
using System;
using System.Data;
namespace EnvelopeGenerator.Jobs;
public static class DataRowExtensions
{
public static T? GetValueOrDefault<T>(this DataRow row, string columnName, T? defaultValue = default)
{
if (!row.Table.Columns.Contains(columnName))
{
return defaultValue;
}
var value = row[columnName];
if (value == DBNull.Value)
{
return defaultValue;
}
try
{
return (T)Convert.ChangeType(value, typeof(T));
}
catch
{
return defaultValue;
}
}
}

View File

@@ -1,28 +0,0 @@
namespace EnvelopeGenerator.Jobs.FinalizeDocument;
public static class FinalizeDocumentExceptions
{
public class MergeDocumentException : ApplicationException
{
public MergeDocumentException(string message) : base(message) { }
public MergeDocumentException(string message, Exception innerException) : base(message, innerException) { }
}
public class BurnAnnotationException : ApplicationException
{
public BurnAnnotationException(string message) : base(message) { }
public BurnAnnotationException(string message, Exception innerException) : base(message, innerException) { }
}
public class CreateReportException : ApplicationException
{
public CreateReportException(string message) : base(message) { }
public CreateReportException(string message, Exception innerException) : base(message, innerException) { }
}
public class ExportDocumentException : ApplicationException
{
public ExportDocumentException(string message) : base(message) { }
public ExportDocumentException(string message, Exception innerException) : base(message, innerException) { }
}
}

View File

@@ -1,229 +0,0 @@
using System.Collections.Generic;
using System.Data;
using System.IO;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using EnvelopeGenerator.Domain.Constants;
using EnvelopeGenerator.Domain.Entities;
using Microsoft.Data.SqlClient;
using Microsoft.Extensions.Logging;
using Quartz;
using static EnvelopeGenerator.Jobs.FinalizeDocument.FinalizeDocumentExceptions;
namespace EnvelopeGenerator.Jobs.FinalizeDocument;
public class FinalizeDocumentJob : IJob
{
private readonly ILogger<FinalizeDocumentJob> _logger;
private readonly PDFBurner _pdfBurner;
private readonly PDFMerger _pdfMerger;
private readonly ReportCreator _reportCreator;
private record ConfigSettings(string DocumentPath, string DocumentPathOrigin, string ExportPath);
public FinalizeDocumentJob(
ILogger<FinalizeDocumentJob> logger,
PDFBurner pdfBurner,
PDFMerger pdfMerger,
ReportCreator reportCreator)
{
_logger = logger;
_pdfBurner = pdfBurner;
_pdfMerger = pdfMerger;
_reportCreator = reportCreator;
}
public async Task Execute(IJobExecutionContext context)
{
var jobId = context.JobDetail.Key.ToString();
_logger.LogDebug("Starting job {JobId}", jobId);
try
{
var connectionString = context.MergedJobDataMap.GetString(Value.DATABASE);
if (string.IsNullOrWhiteSpace(connectionString))
{
_logger.LogWarning("FinalizeDocument - Connection string missing");
return;
}
await using var connection = new SqlConnection(connectionString);
await connection.OpenAsync(context.CancellationToken);
var config = await LoadConfigurationAsync(connection, context.CancellationToken);
var envelopes = await LoadCompletedEnvelopesAsync(connection, context.CancellationToken);
if (envelopes.Count == 0)
{
_logger.LogInformation("No completed envelopes found");
return;
}
var total = envelopes.Count;
var current = 1;
foreach (var envelopeId in envelopes)
{
_logger.LogInformation("Finalizing Envelope {EnvelopeId} ({Current}/{Total})", envelopeId, current, total);
try
{
var envelopeData = await GetEnvelopeDataAsync(connection, envelopeId, context.CancellationToken);
if (envelopeData is null)
{
_logger.LogWarning("Envelope data not found for {EnvelopeId}", envelopeId);
continue;
}
var data = envelopeData.Value;
var envelope = new Envelope
{
Id = envelopeId,
Uuid = data.EnvelopeUuid ?? string.Empty,
Title = data.Title ?? string.Empty,
FinalEmailToCreator = (int)FinalEmailType.No,
FinalEmailToReceivers = (int)FinalEmailType.No
};
var burned = _pdfBurner.BurnAnnotsToPDF(data.DocumentBytes, data.AnnotationData, envelopeId);
var report = _reportCreator.CreateReport(connection, envelope);
var merged = _pdfMerger.MergeDocuments(burned, report);
var outputDirectory = Path.Combine(config.ExportPath, data.ParentFolderUid);
Directory.CreateDirectory(outputDirectory);
var outputPath = Path.Combine(outputDirectory, $"{envelope.Uuid}.pdf");
await File.WriteAllBytesAsync(outputPath, merged, context.CancellationToken);
await UpdateDocumentResultAsync(connection, envelopeId, merged, context.CancellationToken);
await ArchiveEnvelopeAsync(connection, envelopeId, context.CancellationToken);
}
catch (MergeDocumentException ex)
{
_logger.LogWarning(ex, "Certificate Document job failed at merging documents");
}
catch (ExportDocumentException ex)
{
_logger.LogWarning(ex, "Certificate Document job failed at exporting document");
}
catch (Exception ex)
{
_logger.LogError(ex, "Unhandled exception while working envelope {EnvelopeId}", envelopeId);
}
current++;
_logger.LogInformation("Envelope {EnvelopeId} finalized", envelopeId);
}
_logger.LogDebug("Completed job {JobId} successfully", jobId);
}
catch (Exception ex)
{
_logger.LogError(ex, "Certificate Document job failed");
}
finally
{
_logger.LogDebug("Job execution for {JobId} ended", jobId);
}
}
private async Task<ConfigSettings> LoadConfigurationAsync(SqlConnection connection, CancellationToken cancellationToken)
{
const string sql = "SELECT TOP 1 DOCUMENT_PATH, EXPORT_PATH FROM TBSIG_CONFIG";
await using var command = new SqlCommand(sql, connection);
await using var reader = await command.ExecuteReaderAsync(cancellationToken);
if (await reader.ReadAsync(cancellationToken))
{
var documentPath = reader.IsDBNull(0) ? string.Empty : reader.GetString(0);
var exportPath = reader.IsDBNull(1) ? string.Empty : reader.GetString(1);
return new ConfigSettings(documentPath, documentPath, exportPath);
}
return new ConfigSettings(string.Empty, string.Empty, Path.GetTempPath());
}
private async Task<List<int>> LoadCompletedEnvelopesAsync(SqlConnection connection, CancellationToken cancellationToken)
{
const string sql = "SELECT GUID FROM TBSIG_ENVELOPE WHERE STATUS = @Status AND DATEDIFF(minute, CHANGED_WHEN, GETDATE()) >= 1 ORDER BY GUID";
var ids = new List<int>();
await using var command = new SqlCommand(sql, connection);
command.Parameters.AddWithValue("@Status", (int)EnvelopeStatus.EnvelopeCompletelySigned);
await using var reader = await command.ExecuteReaderAsync(cancellationToken);
while (await reader.ReadAsync(cancellationToken))
{
ids.Add(reader.GetInt32(0));
}
return ids;
}
private async Task<(int EnvelopeId, string? EnvelopeUuid, string? Title, byte[] DocumentBytes, List<string> AnnotationData, string ParentFolderUid)?> GetEnvelopeDataAsync(SqlConnection connection, int envelopeId, CancellationToken cancellationToken)
{
const string sql = @"SELECT T.GUID, T.ENVELOPE_UUID, T.TITLE, T2.FILEPATH, T2.BYTE_DATA FROM [dbo].[TBSIG_ENVELOPE] T
JOIN TBSIG_ENVELOPE_DOCUMENT T2 ON T.GUID = T2.ENVELOPE_ID
WHERE T.GUID = @EnvelopeId";
await using var command = new SqlCommand(sql, connection);
command.Parameters.AddWithValue("@EnvelopeId", envelopeId);
await using var reader = await command.ExecuteReaderAsync(CommandBehavior.SingleRow, cancellationToken);
if (!await reader.ReadAsync(cancellationToken))
{
return null;
}
var envelopeUuid = reader.IsDBNull(1) ? string.Empty : reader.GetString(1);
var title = reader.IsDBNull(2) ? string.Empty : reader.GetString(2);
var filePath = reader.IsDBNull(3) ? string.Empty : reader.GetString(3);
var bytes = reader.IsDBNull(4) ? Array.Empty<byte>() : (byte[])reader[4];
await reader.CloseAsync();
if (bytes.Length == 0 && !string.IsNullOrWhiteSpace(filePath) && File.Exists(filePath))
{
bytes = await File.ReadAllBytesAsync(filePath, cancellationToken);
}
var annotations = await GetAnnotationDataAsync(connection, envelopeId, cancellationToken);
var parentFolderUid = !string.IsNullOrWhiteSpace(filePath)
? Path.GetFileName(Path.GetDirectoryName(filePath) ?? string.Empty)
: envelopeUuid;
return (envelopeId, envelopeUuid, title, bytes, annotations, parentFolderUid ?? envelopeUuid ?? envelopeId.ToString());
}
private async Task<List<string>> GetAnnotationDataAsync(SqlConnection connection, int envelopeId, CancellationToken cancellationToken)
{
const string sql = "SELECT VALUE FROM TBSIG_DOCUMENT_STATUS WHERE ENVELOPE_ID = @EnvelopeId";
var result = new List<string>();
await using var command = new SqlCommand(sql, connection);
command.Parameters.AddWithValue("@EnvelopeId", envelopeId);
await using var reader = await command.ExecuteReaderAsync(cancellationToken);
while (await reader.ReadAsync(cancellationToken))
{
if (!reader.IsDBNull(0))
{
result.Add(reader.GetString(0));
}
}
return result;
}
private static async Task UpdateDocumentResultAsync(SqlConnection connection, int envelopeId, byte[] bytes, CancellationToken cancellationToken)
{
const string sql = "UPDATE TBSIG_ENVELOPE SET DOC_RESULT = @ImageData WHERE GUID = @EnvelopeId";
await using var command = new SqlCommand(sql, connection);
command.Parameters.AddWithValue("@ImageData", bytes);
command.Parameters.AddWithValue("@EnvelopeId", envelopeId);
await command.ExecuteNonQueryAsync(cancellationToken);
}
private static async Task ArchiveEnvelopeAsync(SqlConnection connection, int envelopeId, CancellationToken cancellationToken)
{
const string sql = "UPDATE TBSIG_ENVELOPE SET STATUS = @Status, CHANGED_WHEN = GETDATE() WHERE GUID = @EnvelopeId";
await using var command = new SqlCommand(sql, connection);
command.Parameters.AddWithValue("@Status", (int)EnvelopeStatus.EnvelopeArchived);
command.Parameters.AddWithValue("@EnvelopeId", envelopeId);
await command.ExecuteNonQueryAsync(cancellationToken);
}
}

View File

@@ -1,277 +0,0 @@
using System.Collections.Generic;
using System.Drawing;
using System.Linq;
using iText.IO.Image;
using iText.Kernel.Colors;
using iText.Kernel.Pdf;
using iText.Kernel.Pdf.Canvas;
using iText.Layout;
using iText.Layout.Element;
using iText.Layout.Font;
using iText.Layout.Properties;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Logging.Abstractions;
using Newtonsoft.Json;
using static EnvelopeGenerator.Jobs.FinalizeDocument.FinalizeDocumentExceptions;
using LayoutImage = iText.Layout.Element.Image;
namespace EnvelopeGenerator.Jobs.FinalizeDocument;
public class PDFBurner
{
private static readonly FontProvider FontProvider = CreateFontProvider();
private readonly ILogger<PDFBurner> _logger;
private readonly PDFBurnerParams _pdfBurnerParams;
public PDFBurner() : this(NullLogger<PDFBurner>.Instance, new PDFBurnerParams())
{
}
public PDFBurner(ILogger<PDFBurner> logger, PDFBurnerParams? pdfBurnerParams = null)
{
_logger = logger;
_pdfBurnerParams = pdfBurnerParams ?? new PDFBurnerParams();
}
public byte[] BurnAnnotsToPDF(byte[] sourceBuffer, IList<string> instantJsonList, int envelopeId)
{
if (sourceBuffer is null || sourceBuffer.Length == 0)
{
throw new BurnAnnotationException("Source document is empty");
}
try
{
using var inputStream = new MemoryStream(sourceBuffer);
using var outputStream = new MemoryStream();
using var reader = new PdfReader(inputStream);
using var writer = new PdfWriter(outputStream);
using var pdf = new PdfDocument(reader, writer);
foreach (var json in instantJsonList ?? Enumerable.Empty<string>())
{
if (string.IsNullOrWhiteSpace(json))
{
continue;
}
var annotationData = JsonConvert.DeserializeObject<AnnotationData>(json);
if (annotationData?.annotations is null)
{
continue;
}
annotationData.annotations.Reverse();
foreach (var annotation in annotationData.annotations)
{
try
{
switch (annotation.type)
{
case AnnotationType.Image:
AddImageAnnotation(pdf, annotation, annotationData.attachments);
break;
case AnnotationType.Ink:
AddInkAnnotation(pdf, annotation);
break;
case AnnotationType.Widget:
var formFieldValue = annotationData.formFieldValues?.FirstOrDefault(fv => fv.name == annotation.id);
if (formFieldValue is not null && !_pdfBurnerParams.IgnoredLabels.Contains(formFieldValue.value))
{
AddFormFieldValue(pdf, annotation, formFieldValue.value);
}
break;
}
}
catch (Exception ex)
{
_logger.LogWarning(ex, "Error applying annotation {AnnotationId} on envelope {EnvelopeId}", annotation.id, envelopeId);
throw new BurnAnnotationException("Adding annotation failed", ex);
}
}
}
pdf.Close();
return outputStream.ToArray();
}
catch (BurnAnnotationException)
{
throw;
}
catch (Exception ex)
{
_logger.LogError(ex, "Failed to burn annotations for envelope {EnvelopeId}", envelopeId);
throw new BurnAnnotationException("Annotations could not be burned", ex);
}
}
private void AddImageAnnotation(PdfDocument pdf, Annotation annotation, Dictionary<string, Attachment>? attachments)
{
if (attachments is null || string.IsNullOrWhiteSpace(annotation.imageAttachmentId) || !attachments.TryGetValue(annotation.imageAttachmentId, out var attachment))
{
return;
}
var page = pdf.GetPage(annotation.pageIndex + 1);
var bounds = annotation.bbox.Select(ToInches).ToList();
var x = (float)bounds[0];
var y = (float)bounds[1];
var width = (float)bounds[2];
var height = (float)bounds[3];
var imageBytes = Convert.FromBase64String(attachment.binary);
var imageData = ImageDataFactory.Create(imageBytes);
var image = new LayoutImage(imageData)
.ScaleAbsolute(width, height)
.SetFixedPosition(annotation.pageIndex + 1, x, y);
using var canvas = new Canvas(new PdfCanvas(page), page.GetPageSize());
canvas.Add(image);
}
private void AddInkAnnotation(PdfDocument pdf, Annotation annotation)
{
if (annotation.lines?.points is null)
{
return;
}
var page = pdf.GetPage(annotation.pageIndex + 1);
var canvas = new PdfCanvas(page);
var color = ParseColor(annotation.strokeColor);
canvas.SetStrokeColor(color);
canvas.SetLineWidth(1);
foreach (var segment in annotation.lines.points)
{
var first = true;
foreach (var point in segment)
{
var (px, py) = (ToInches(point[0]), ToInches(point[1]));
if (first)
{
canvas.MoveTo(px, py);
first = false;
}
else
{
canvas.LineTo(px, py);
}
}
canvas.Stroke();
}
}
private static FontProvider CreateFontProvider()
{
var provider = new FontProvider();
provider.AddStandardPdfFonts();
provider.AddSystemFonts();
return provider;
}
private void AddFormFieldValue(PdfDocument pdf, Annotation annotation, string value)
{
var bounds = annotation.bbox.Select(ToInches).ToList();
var x = (float)bounds[0];
var y = (float)bounds[1];
var width = (float)bounds[2];
var height = (float)bounds[3];
var page = pdf.GetPage(annotation.pageIndex + 1);
var canvas = new Canvas(new PdfCanvas(page), page.GetPageSize());
canvas.SetProperty(Property.FONT_PROVIDER, FontProvider);
canvas.SetProperty(Property.FONT, FontProvider.GetFontSet());
var paragraph = new Paragraph(value)
.SetFontSize(_pdfBurnerParams.FontSize)
.SetFontColor(ColorConstants.BLACK)
.SetFontFamily(_pdfBurnerParams.FontName);
if (_pdfBurnerParams.FontStyle.HasFlag(FontStyle.Italic))
{
paragraph.SetItalic();
}
if (_pdfBurnerParams.FontStyle.HasFlag(FontStyle.Bold))
{
paragraph.SetBold();
}
canvas.ShowTextAligned(
paragraph,
x + (float)_pdfBurnerParams.TopMargin,
y + (float)_pdfBurnerParams.YOffset,
annotation.pageIndex + 1,
iText.Layout.Properties.TextAlignment.LEFT,
iText.Layout.Properties.VerticalAlignment.TOP,
0);
}
private static DeviceRgb ParseColor(string? color)
{
if (string.IsNullOrWhiteSpace(color))
{
return new DeviceRgb(0, 0, 0);
}
try
{
var drawingColor = ColorTranslator.FromHtml(color);
return new DeviceRgb(drawingColor.R, drawingColor.G, drawingColor.B);
}
catch
{
return new DeviceRgb(0, 0, 0);
}
}
private static double ToInches(double value) => value / 72d;
private static double ToInches(float value) => value / 72d;
#region Model
private static class AnnotationType
{
public const string Image = "pspdfkit/image";
public const string Ink = "pspdfkit/ink";
public const string Widget = "pspdfkit/widget";
}
private sealed class AnnotationData
{
public List<Annotation>? annotations { get; set; }
public Dictionary<string, Attachment>? attachments { get; set; }
public List<FormFieldValue>? formFieldValues { get; set; }
}
private sealed class Annotation
{
public string id { get; set; } = string.Empty;
public List<double> bbox { get; set; } = new();
public string type { get; set; } = string.Empty;
public string imageAttachmentId { get; set; } = string.Empty;
public Lines? lines { get; set; }
public int pageIndex { get; set; }
public string strokeColor { get; set; } = string.Empty;
public string egName { get; set; } = string.Empty;
}
private sealed class Lines
{
public List<List<List<float>>> points { get; set; } = new();
}
private sealed class Attachment
{
public string binary { get; set; } = string.Empty;
public string contentType { get; set; } = string.Empty;
}
private sealed class FormFieldValue
{
public string name { get; set; } = string.Empty;
public string value { get; set; } = string.Empty;
}
#endregion
}

View File

@@ -1,19 +0,0 @@
using System.Collections.Generic;
using System.Drawing;
namespace EnvelopeGenerator.Jobs.FinalizeDocument;
public class PDFBurnerParams
{
public List<string> IgnoredLabels { get; } = new() { "Date", "Datum", "ZIP", "PLZ", "Place", "Ort", "Position", "Stellung" };
public double TopMargin { get; set; } = 0.1;
public double YOffset { get; set; } = -0.3;
public string FontName { get; set; } = "Arial";
public int FontSize { get; set; } = 8;
public FontStyle FontStyle { get; set; } = FontStyle.Italic;
}

View File

@@ -1,46 +0,0 @@
using System.IO;
using iText.Kernel.Pdf;
using iText.Kernel.Utils;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Logging.Abstractions;
using static EnvelopeGenerator.Jobs.FinalizeDocument.FinalizeDocumentExceptions;
namespace EnvelopeGenerator.Jobs.FinalizeDocument;
public class PDFMerger
{
private readonly ILogger<PDFMerger> _logger;
public PDFMerger() : this(NullLogger<PDFMerger>.Instance)
{
}
public PDFMerger(ILogger<PDFMerger> logger)
{
_logger = logger;
}
public byte[] MergeDocuments(byte[] document, byte[] report)
{
try
{
using var finalStream = new MemoryStream();
using var documentReader = new PdfReader(new MemoryStream(document));
using var reportReader = new PdfReader(new MemoryStream(report));
using var writer = new PdfWriter(finalStream);
using var targetDoc = new PdfDocument(documentReader, writer);
using var reportDoc = new PdfDocument(reportReader);
var merger = new PdfMerger(targetDoc);
merger.Merge(reportDoc, 1, reportDoc.GetNumberOfPages());
targetDoc.Close();
return finalStream.ToArray();
}
catch (Exception ex)
{
_logger.LogError(ex, "Failed to merge PDF documents");
throw new MergeDocumentException("Documents could not be merged", ex);
}
}
}

View File

@@ -1,91 +0,0 @@
using System.Data;
using System.IO;
using EnvelopeGenerator.Domain.Entities;
using iText.Kernel.Pdf;
using iText.Layout.Element;
using Microsoft.Data.SqlClient;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Logging.Abstractions;
using static EnvelopeGenerator.Jobs.FinalizeDocument.FinalizeDocumentExceptions;
using LayoutDocument = iText.Layout.Document;
namespace EnvelopeGenerator.Jobs.FinalizeDocument;
public class ReportCreator
{
private readonly ILogger<ReportCreator> _logger;
public ReportCreator() : this(NullLogger<ReportCreator>.Instance)
{
}
public ReportCreator(ILogger<ReportCreator> logger)
{
_logger = logger;
}
public byte[] CreateReport(SqlConnection connection, Envelope envelope)
{
try
{
var reportItems = LoadReportItems(connection, envelope.Id);
using var stream = new MemoryStream();
using var writer = new PdfWriter(stream);
using var pdf = new PdfDocument(writer);
using var document = new LayoutDocument(pdf);
document.Add(new Paragraph("Envelope Finalization Report").SetFontSize(16));
document.Add(new Paragraph($"Envelope Id: {envelope.Id}"));
document.Add(new Paragraph($"UUID: {envelope.Uuid}"));
document.Add(new Paragraph($"Title: {envelope.Title}"));
document.Add(new Paragraph($"Subject: {envelope.Comment}"));
document.Add(new Paragraph($"Generated: {DateTime.UtcNow:O}"));
document.Add(new Paragraph(" "));
var table = new Table(4).UseAllAvailableWidth();
table.AddHeaderCell("Date");
table.AddHeaderCell("Status");
table.AddHeaderCell("User");
table.AddHeaderCell("EnvelopeId");
foreach (var item in reportItems.OrderByDescending(r => r.ItemDate))
{
table.AddCell(item.ItemDate.ToString("u"));
table.AddCell(item.ItemStatus.ToString());
table.AddCell(item.ItemUserReference);
table.AddCell(item.EnvelopeId.ToString());
}
document.Add(table);
document.Close();
return stream.ToArray();
}
catch (Exception ex)
{
_logger.LogError(ex, "Could not create report for envelope {EnvelopeId}", envelope.Id);
throw new CreateReportException("Could not prepare report data", ex);
}
}
private List<ReportItem> LoadReportItems(SqlConnection connection, int envelopeId)
{
const string sql = "SELECT ENVELOPE_ID, POS_WHEN, POS_STATUS, POS_WHO FROM VWSIG_ENVELOPE_REPORT WHERE ENVELOPE_ID = @EnvelopeId";
var result = new List<ReportItem>();
using var command = new SqlCommand(sql, connection);
command.Parameters.AddWithValue("@EnvelopeId", envelopeId);
using var reader = command.ExecuteReader();
while (reader.Read())
{
result.Add(new ReportItem
{
EnvelopeId = reader.GetInt32(0),
ItemDate = reader.IsDBNull(1) ? DateTime.MinValue : reader.GetDateTime(1),
ItemStatus = reader.IsDBNull(2) ? default : (EnvelopeGenerator.Domain.Constants.EnvelopeStatus)reader.GetInt32(2),
ItemUserReference = reader.IsDBNull(3) ? string.Empty : reader.GetString(3)
});
}
return result;
}
}

View File

@@ -1,19 +0,0 @@
using EnvelopeGenerator.Domain.Constants;
using EnvelopeGenerator.Domain.Entities;
namespace EnvelopeGenerator.Jobs.FinalizeDocument;
public class ReportItem
{
public Envelope? Envelope { get; set; }
public int EnvelopeId { get; set; }
public string EnvelopeTitle { get; set; } = string.Empty;
public string EnvelopeSubject { get; set; } = string.Empty;
public EnvelopeStatus ItemStatus { get; set; }
public string ItemStatusTranslated => ItemStatus.ToString();
public string ItemUserReference { get; set; } = string.Empty;
public DateTime ItemDate { get; set; }
}

View File

@@ -1,8 +0,0 @@
using System.Collections.Generic;
namespace EnvelopeGenerator.Jobs.FinalizeDocument;
public class ReportSource
{
public List<ReportItem> Items { get; set; } = new();
}

View File

@@ -105,17 +105,13 @@ namespace EnvelopeGenerator.PdfEditor
public Pdf<TInputStream, TOutputStream> Background<TSignature>(IEnumerable<TSignature> signatures, double widthPx = 1.9500000000000002, double heightPx = 2.52) public Pdf<TInputStream, TOutputStream> Background<TSignature>(IEnumerable<TSignature> signatures, double widthPx = 1.9500000000000002, double heightPx = 2.52)
where TSignature : ISignature where TSignature : ISignature
{ {
// once per page
Page(page =>
{
var canvas = new PdfCanvas(page);
canvas.ConcatMatrix(1, 0, 0, -1, 0, page.GetPageSize().GetHeight());
});
foreach (var signature in signatures) foreach (var signature in signatures)
Page(signature.Page, page => Page(signature.Page, page =>
{ {
var canvas = new PdfCanvas(page); var canvas = new PdfCanvas(page);
canvas.SaveState();
canvas.ConcatMatrix(1, 0, 0, -1, 0, page.GetPageSize().GetHeight());
double inchFactor = 72; double inchFactor = 72;
double magin = .2; double magin = .2;
double x = (signature.X - .7 - magin) * inchFactor; double x = (signature.X - .7 - magin) * inchFactor;
@@ -134,6 +130,8 @@ namespace EnvelopeGenerator.PdfEditor
canvas.SetFillColor(new DeviceRgb(204, 202, 198)) canvas.SetFillColor(new DeviceRgb(204, 202, 198))
.Rectangle(x, y + height - bottomLineLength, width, bottomLineLength) .Rectangle(x, y + height - bottomLineLength, width, bottomLineLength)
.Fill(); .Fill();
canvas.RestoreState();
}); });
return this; return this;

View File

@@ -1,46 +0,0 @@
using System.Security.Claims;
using Microsoft.AspNetCore.Components.Authorization;
using EnvelopeGenerator.ReceiverUI.Client.Services;
namespace EnvelopeGenerator.ReceiverUI.Client.Auth;
/// <summary>
/// Fragt die API, ob der Nutzer eingeloggt ist.
///
/// WARUM nicht selbst Token lesen?
/// - Das Auth-Cookie ist HttpOnly → JavaScript/WASM kann es nicht lesen
/// - Stattdessen: Frage die API "bin ich eingeloggt?" → GET /api/auth/check
/// - Die API prüft das Cookie serverseitig und antwortet mit 200 oder 401
/// </summary>
public class ApiAuthStateProvider : AuthenticationStateProvider
{
private readonly IAuthService _authService;
public ApiAuthStateProvider(IAuthService authService)
{
_authService = authService;
}
public override async Task<AuthenticationState> GetAuthenticationStateAsync()
{
var result = await _authService.CheckAuthAsync();
if (result.IsSuccess)
{
// Eingeloggt → Erstelle einen authentifizierten ClaimsPrincipal
var identity = new ClaimsIdentity("cookie");
return new AuthenticationState(new ClaimsPrincipal(identity));
}
// Nicht eingeloggt
return new AuthenticationState(new ClaimsPrincipal(new ClaimsIdentity()));
}
/// <summary>
/// Wird nach Login/Logout aufgerufen, damit Blazor den Auth-State aktualisiert.
/// </summary>
public void NotifyAuthChanged()
{
NotifyAuthenticationStateChanged(GetAuthenticationStateAsync());
}
}

View File

@@ -1,172 +0,0 @@
@* AccessCodeForm: Zeigt das Zugangscode-Eingabeformular.
Entspricht dem AccessCode-Teil von EnvelopeLocked.cshtml im Web-Projekt.
"Dumme" Komponente: Keine Services, keine API-Calls.
- Bekommt Daten über [Parameter]
- Gibt Eingaben über EventCallback an die Eltern-Page zurück
- Die Page entscheidet, was mit dem Code passiert *@
<div class="page">
@* ── Header: Icon + Titel ── *@
<header class="text-center">
<div class="status-icon locked mt-4 mb-1">
<i class="bi bi-shield-lock"></i>
</div>
<h1>Zugangscode eingeben</h1>
</header>
@* ── Erklärungstext ── *@
<section class="text-center mb-4">
<p class="text-muted">
Ein Zugangscode wurde an Ihre E-Mail-Adresse gesendet.
Bitte geben Sie diesen unten ein.
</p>
</section>
@* ── Formular ── *@
<div class="access-code-container">
<EditForm Model="_model" OnValidSubmit="Submit">
<DataAnnotationsValidator />
@* Code-Eingabefeld: type="password" damit niemand mitlesen kann *@
<div class="form-floating mb-3">
<InputText @bind-Value="_model.Code"
type="password"
class="form-control code-input"
id="accessCodeInput"
placeholder="Zugangscode" />
<label for="accessCodeInput">Zugangscode</label>
<ValidationMessage For="() => _model.Code" />
</div>
@* TFA-Switch: Nur sichtbar wenn der Umschlag TFA aktiviert hat.
Im Web-Projekt ist das der "2FA per SMS"-Toggle.
Disabled wenn der Empfänger keine Telefonnummer hat. *@
@if (TfaEnabled)
{
<div class="form-check form-switch mb-3">
<input class="form-check-input"
type="checkbox"
id="preferSmsSwitch"
checked="@_preferSms"
disabled="@(!HasPhoneNumber)"
@onchange="ToggleSms" />
<label class="form-check-label" for="preferSmsSwitch">
2FA per SMS
</label>
</div>
}
@* Fehlermeldung: Wird von der Eltern-Page gesetzt
wenn der AccessCode falsch war *@
@if (!string.IsNullOrEmpty(ErrorMessage))
{
<div class="alert alert-danger">@ErrorMessage</div>
}
@* Submit-Button mit Loading-State *@
<button type="submit" class="btn btn-primary w-100" disabled="@_isSubmitting">
@if (_isSubmitting)
{
<LoadingIndicator Small="true" />
}
else
{
<i class="bi bi-box-arrow-in-right me-2"></i>
<span>Bestätigen</span>
}
</button>
</EditForm>
</div>
@* ── Hilfe-Bereich: Wer hat dieses Dokument gesendet? ──
Im Web-Projekt ist das der <details>-Bereich ganz unten.
Zeigt Sender-Mail und Umschlag-Titel, damit der Empfänger
bei Problemen weiß, an wen er sich wenden kann. *@
@if (!string.IsNullOrEmpty(SenderEmail))
{
<section class="text-center mt-4">
<details>
<summary class="text-muted">Woher kommt dieser Code?</summary>
<p class="text-muted mt-2">
Dieses Dokument
@if (!string.IsNullOrEmpty(Title))
{
<span>«@Title» </span>
}
wurde Ihnen von <a href="mailto:@SenderEmail">@SenderEmail</a> zugesendet.
Der Zugangscode wurde ebenfalls an Ihre E-Mail-Adresse geschickt.
</p>
</details>
</section>
}
</div>
@code {
// ── Parameter von der Eltern-Page ──
/// <summary>Der Envelope-Key aus der URL</summary>
[Parameter, EditorRequired]
public string EnvelopeKey { get; set; } = string.Empty;
/// <summary>Fehlermeldung (z.B. "Falscher Zugangscode")</summary>
[Parameter]
public string? ErrorMessage { get; set; }
/// <summary>E-Mail des Absenders — für den Hilfe-Bereich unten</summary>
[Parameter]
public string? SenderEmail { get; set; }
/// <summary>Titel des Umschlags — für den Hilfe-Bereich unten</summary>
[Parameter]
public string? Title { get; set; }
/// <summary>Ob TFA für diesen Umschlag aktiviert ist — zeigt den SMS-Switch</summary>
[Parameter]
public bool TfaEnabled { get; set; }
/// <summary>Ob der Empfänger eine Telefonnummer hat — sonst ist SMS-Switch disabled</summary>
[Parameter]
public bool HasPhoneNumber { get; set; }
// ── Event: Gibt Code + SMS-Präferenz an die Page zurück ──
/// <summary>
/// Callback wenn der Benutzer das Formular abschickt.
/// Gibt ein Tuple zurück: (Code, PreferSms)
/// Die Page entscheidet dann, was damit passiert (API-Call etc.)
///
/// WARUM ein Tuple statt nur string?
/// Weil die Page auch wissen muss, ob der Benutzer SMS bevorzugt.
/// Im Web-Projekt wird das als separates Form-Feld "userSelectSMS" gesendet.
/// </summary>
[Parameter]
public EventCallback<(string Code, bool PreferSms)> OnSubmit { get; set; }
// ── Interner State ──
private AccessCodeModel _model = new();
private bool _isSubmitting;
private bool _preferSms;
private void ToggleSms(ChangeEventArgs e)
{
_preferSms = (bool)(e.Value ?? false);
}
private async Task Submit()
{
_isSubmitting = true;
await OnSubmit.InvokeAsync((_model.Code, _preferSms));
_isSubmitting = false;
}
// ── Validierungs-Model ──
private class AccessCodeModel
{
[System.ComponentModel.DataAnnotations.Required(ErrorMessage = "Bitte Zugangscode eingeben")]
[System.ComponentModel.DataAnnotations.StringLength(6, MinimumLength = 4)]
public string Code { get; set; } = string.Empty;
}
}

View File

@@ -1,126 +0,0 @@
@* ActionPanel: Fixierte Button-Leiste am unteren Bildschirmrand.
Entspricht dem <div id="flex-action-panel"> in ShowEnvelope.cshtml.
Wird angezeigt wenn der Empfänger das Dokument sieht (Status = ShowDocument).
Bei ReadOnly-Dokumenten wird nichts angezeigt.
Die Buttons lösen EventCallbacks aus. Die Eltern-Komponente (EnvelopePage)
entscheidet, was passiert (API-Call etc.).
Der ConfirmDialog wird über eine Referenz aufgerufen:
var confirmed = await _confirmDialog.ShowAsync("Titel", "Text");
WARUM EventCallbacks statt direkte API-Calls?
- Die Komponente bleibt "dumm" (keine Services, kein API-Wissen)
- Die Eltern-Page kann den gleichen Button-Click anders behandeln
- Einfacher zu testen *@
@if (!ReadOnly)
{
@* position-fixed: Bleibt am unteren Rand auch beim Scrollen.
Gleiche Positionierung wie im Web-Projekt. *@
<div class="position-fixed bottom-0 end-0 p-3 d-flex gap-2" style="z-index: 1050;">
@* Zurücksetzen-Button: Setzt alle Signaturen zurück.
Im Web-Projekt ist das der graue Button mit dem
Pfeil-gegen-den-Uhrzeigersinn-Icon. *@
<button class="btn btn-secondary" @onclick="HandleRefresh" title="Zurücksetzen">
<i class="bi bi-arrow-counterclockwise"></i>
</button>
@* Ablehnen-Button: Öffnet ConfirmDialog, dann EventCallback.
Im Web-Projekt ist das der rote Button mit dem X-Icon. *@
<button class="btn btn-danger" @onclick="HandleReject" title="Ablehnen">
<i class="bi bi-x-lg me-1"></i>
<span class="d-none d-md-inline">Ablehnen</span>
</button>
@* Unterschreiben-Button: Öffnet ConfirmDialog, dann EventCallback.
Im Web-Projekt ist das der grüne Button mit dem Briefumschlag-Icon. *@
<button class="btn btn-success" @onclick="HandleSign" title="Unterschreiben">
<i class="bi bi-pen me-1"></i>
<span class="d-none d-md-inline">Unterschreiben</span>
</button>
</div>
}
@* ConfirmDialog: Wird nur gerendert wenn nötig (wenn ShowAsync aufgerufen wird).
Die Referenz (_confirmDialog) erlaubt uns, ShowAsync von den Button-Handlern aufzurufen. *@
<ConfirmDialog @ref="_confirmDialog" />
@code {
// ── Parameter ──
/// <summary>Bei ReadOnly wird das gesamte Panel ausgeblendet</summary>
[Parameter]
public bool ReadOnly { get; set; }
/// <summary>Wird ausgelöst wenn der Benutzer "Unterschreiben" bestätigt</summary>
[Parameter]
public EventCallback OnSign { get; set; }
/// <summary>Wird ausgelöst wenn der Benutzer "Ablehnen" bestätigt</summary>
[Parameter]
public EventCallback OnReject { get; set; }
/// <summary>Wird ausgelöst wenn der Benutzer "Zurücksetzen" klickt</summary>
[Parameter]
public EventCallback OnRefresh { get; set; }
// ── Referenz auf den ConfirmDialog ──
/// <summary>
/// Referenz auf die ConfirmDialog-Komponente.
///
/// WAS IST @ref?
/// Mit @ref speichert Blazor eine Referenz auf eine Kind-Komponente.
/// Dann kann man Methoden darauf aufrufen: _confirmDialog.ShowAsync(...)
/// Das ist wie document.getElementById() in JavaScript, nur typsicher.
/// </summary>
private ConfirmDialog _confirmDialog = default!;
// ── Button-Handler ──
/// <summary>
/// Unterschreiben: Erst bestätigen, dann Event auslösen.
/// Der ConfirmDialog zeigt "Möchten Sie das Dokument unterschreiben?"
/// Nur wenn der Benutzer "Ja" klickt, wird OnSign ausgelöst.
/// </summary>
private async Task HandleSign()
{
var confirmed = await _confirmDialog.ShowAsync(
"Unterschreiben",
"Möchten Sie das Dokument wirklich unterschreiben? Diese Aktion kann nicht rückgängig gemacht werden.",
confirmText: "Unterschreiben",
confirmColor: "success");
if (confirmed)
await OnSign.InvokeAsync();
}
/// <summary>
/// Ablehnen: Erst bestätigen, dann Event auslösen.
/// Roter Button im ConfirmDialog, weil Ablehnen destruktiv ist.
/// </summary>
private async Task HandleReject()
{
var confirmed = await _confirmDialog.ShowAsync(
"Ablehnen",
"Möchten Sie das Dokument wirklich ablehnen? Diese Aktion kann nicht rückgängig gemacht werden.",
confirmText: "Ablehnen",
confirmColor: "danger");
if (confirmed)
await OnReject.InvokeAsync();
}
/// <summary>
/// Zurücksetzen: Kein ConfirmDialog nötig, weil es nicht destruktiv ist.
/// Setzt nur die Signatur-Positionen zurück, der Empfänger kann danach
/// erneut signieren.
/// </summary>
private async Task HandleRefresh()
{
await OnRefresh.InvokeAsync();
}
}

View File

@@ -1,121 +0,0 @@
@* EnvelopeInfoCard: Zeigt die Umschlag-Infos oberhalb des PDF-Viewers.
Entspricht dem Card-Bereich in ShowEnvelope.cshtml im Web-Projekt.
Wird angezeigt wenn der Empfänger erfolgreich authentifiziert ist
und das Dokument sehen darf.
"Dumme" Komponente: Keine Services, nur Parameter → Anzeige. *@
<div class="card mb-3">
@* ── Card Header: Logo + Fortschrittsbalken ──
Im Web-Projekt gibt es hier das signFLOW-Logo und darunter
"2/3 Signatures". Wir zeigen den Fortschritt nur wenn es
KEIN ReadOnly-Dokument ist (ReadOnly = nur lesen, nicht signieren). *@
<div class="card-header bg-white">
<div class="d-flex justify-content-between align-items-center">
<strong class="text-primary">signFLOW</strong>
@if (!ReadOnly && SignatureTotal > 0)
{
<div class="d-flex align-items-center gap-2">
<div class="progress" style="width: 120px; height: 8px;">
<div class="progress-bar bg-success"
role="progressbar"
style="width: @ProgressPercent%"
aria-valuenow="@SignaturesDone"
aria-valuemin="0"
aria-valuemax="@SignatureTotal">
</div>
</div>
<small class="text-muted">@SignaturesDone/@SignatureTotal</small>
</div>
}
@if (ReadOnly)
{
<span class="badge bg-secondary">Nur Ansicht</span>
}
</div>
</div>
@* ── Card Body: Titel, Nachricht, Sender-Info ── *@
<div class="card-body">
@* Titel des Umschlags *@
<h5 class="card-title">@Title</h5>
@* Nachricht des Absenders — im Web-Projekt wird das mit Marked.js
als Markdown gerendert. Hier erstmal als Plain Text.
Markdown-Rendering kommt in Phase 6 (Feinschliff). *@
@if (!string.IsNullOrEmpty(Message))
{
<p class="card-text">@Message</p>
}
@* Sender-Info: Wer hat es gesendet, wann?
Im Web-Projekt steht hier:
"Gesendet am 15.03.2026 von Max Mustermann (max@firma.de)" *@
<p class="card-text">
<small class="text-muted">
@if (!string.IsNullOrEmpty(SenderName) && !string.IsNullOrEmpty(SenderEmail))
{
<span>
Gesendet
@if (SentDate is not null)
{
<span>am @SentDate.Value.ToString("dd.MM.yyyy")</span>
}
von @SenderName
(<a href="mailto:@SenderEmail">@SenderEmail</a>)
</span>
}
</small>
</p>
</div>
</div>
@code {
// ── Parameter ──
/// <summary>Titel des Umschlags (z.B. "Vertragsdokument")</summary>
[Parameter, EditorRequired]
public string Title { get; set; } = string.Empty;
/// <summary>Nachricht des Absenders</summary>
[Parameter]
public string? Message { get; set; }
/// <summary>Name des Absenders (z.B. "Max Mustermann")</summary>
[Parameter]
public string? SenderName { get; set; }
/// <summary>E-Mail des Absenders</summary>
[Parameter]
public string? SenderEmail { get; set; }
/// <summary>Datum an dem der Umschlag gesendet wurde</summary>
[Parameter]
public DateTime? SentDate { get; set; }
/// <summary>Ob das Dokument nur zum Lesen ist (kein Signieren)</summary>
[Parameter]
public bool ReadOnly { get; set; }
/// <summary>Anzahl bereits geleisteter Unterschriften</summary>
[Parameter]
public int SignaturesDone { get; set; }
/// <summary>Gesamtanzahl benötigter Unterschriften</summary>
[Parameter]
public int SignatureTotal { get; set; }
// ── Berechnete Werte ──
/// <summary>
/// Fortschritt in Prozent für den Balken.
/// Vermeidet Division durch Null.
/// </summary>
private int ProgressPercent =>
SignatureTotal > 0
? (int)((double)SignaturesDone / SignatureTotal * 100)
: 0;
}

View File

@@ -1,23 +0,0 @@
@inject IJSRuntime JS
@implements IAsyncDisposable
<div id="pspdfkit-container" class="pdf-container" style="width: 100%; height: 80vh;"></div>
@code {
[Parameter] public byte[]? DocumentBytes { get; set; }
protected override async Task OnAfterRenderAsync(bool firstRender)
{
if (firstRender && DocumentBytes is not null)
{
// TODO: PSPDFKit JS-Interop implementieren (Phase 6)
// await JS.InvokeVoidAsync("initPdfViewer", DocumentBytes);
}
}
public async ValueTask DisposeAsync()
{
// TODO: PSPDFKit aufräumen
// await JS.InvokeVoidAsync("destroyPdfViewer");
}
}

View File

@@ -1,202 +0,0 @@
@* TfaForm: Zwei-Faktor-Authentifizierung — SMS oder Authenticator.
Entspricht dem TFA-Teil von EnvelopeLocked.cshtml im Web-Projekt.
Wird angezeigt NACH dem AccessCode-Schritt, wenn der Umschlag TFA erfordert.
Zwei Varianten:
- TfaType="sms" → SMS wurde gesendet, Countdown-Timer zeigt verbleibende Zeit
- TfaType="authenticator" → Benutzer gibt Code aus seiner Authenticator-App ein
"Dumme" Komponente: Keine Services, keine API-Calls. *@
@implements IDisposable
<div class="page">
@* ── Header: Icon + Titel ── *@
<header class="text-center">
<div class="status-icon locked mt-4 mb-1">
<i class="bi bi-shield-check"></i>
</div>
<h1>Zwei-Faktor-Authentifizierung</h1>
</header>
@* ── Erklärungstext: Unterschiedlich je nach TFA-Typ ── *@
<section class="text-center mb-4">
@if (TfaType == "sms")
{
<p class="text-muted">
Ein Bestätigungscode wurde per SMS an Ihre Telefonnummer gesendet.
</p>
@* SMS-Countdown-Timer: Zeigt wie lange der Code noch gültig ist.
Im Web-Projekt ist das JavaScript (setInterval).
Hier machen wir es mit einem C#-Timer — kein JS nötig. *@
@if (_remainingTime is not null)
{
<div class="alert @(_remainingTime > TimeSpan.Zero ? "alert-primary" : "alert-warning")">
@if (_remainingTime > TimeSpan.Zero)
{
<span>Code gültig für: @_remainingTime.Value.ToString("mm\\:ss")</span>
}
else
{
<span>Code abgelaufen. Bitte fordern Sie einen neuen Code an.</span>
}
</div>
}
}
else
{
<p class="text-muted">
Öffnen Sie Ihre Authenticator-App und geben Sie den angezeigten Code ein.
</p>
}
</section>
@* ── Formular ── *@
<div class="access-code-container">
<EditForm Model="_model" OnValidSubmit="Submit">
<DataAnnotationsValidator />
<div class="form-floating mb-3">
<InputText @bind-Value="_model.Code"
type="password"
class="form-control code-input"
id="tfaCodeInput"
placeholder="Code" />
<label for="tfaCodeInput">
@(TfaType == "sms" ? "SMS-Code" : "Authenticator-Code")
</label>
<ValidationMessage For="() => _model.Code" />
</div>
@if (!string.IsNullOrEmpty(ErrorMessage))
{
<div class="alert alert-danger">@ErrorMessage</div>
}
<button type="submit"
class="btn btn-primary w-100"
disabled="@(_isSubmitting || _remainingTime <= TimeSpan.Zero)">
@if (_isSubmitting)
{
<LoadingIndicator Small="true" />
}
else
{
<i class="bi bi-unlock me-2"></i>
<span>Bestätigen</span>
}
</button>
</EditForm>
</div>
</div>
@code {
// ── Parameter von der Eltern-Page ──
/// <summary>Der Envelope-Key aus der URL</summary>
[Parameter, EditorRequired]
public string EnvelopeKey { get; set; } = string.Empty;
/// <summary>
/// "sms" oder "authenticator" — bestimmt welche Variante angezeigt wird.
/// Kommt aus der API-Antwort nach dem AccessCode-Schritt.
/// </summary>
[Parameter, EditorRequired]
public string TfaType { get; set; } = "authenticator";
/// <summary>
/// Ablaufzeit des SMS-Codes. Nur bei TfaType="sms" relevant.
/// Der Timer zählt von jetzt bis zu diesem Zeitpunkt runter.
/// </summary>
[Parameter]
public DateTime? TfaExpiration { get; set; }
/// <summary>Ob der Empfänger eine Telefonnummer hat</summary>
[Parameter]
public bool HasPhoneNumber { get; set; }
/// <summary>Fehlermeldung (z.B. "Falscher Code")</summary>
[Parameter]
public string? ErrorMessage { get; set; }
/// <summary>
/// Callback: Gibt (Code, Type) an die Page zurück.
/// Die Page sendet das dann an die API.
/// </summary>
[Parameter]
public EventCallback<(string Code, string Type)> OnSubmit { get; set; }
// ── Interner State ──
private TfaCodeModel _model = new();
private bool _isSubmitting;
private TimeSpan? _remainingTime;
private System.Threading.Timer? _timer;
// ── Lifecycle ──
/// <summary>
/// Wird aufgerufen wenn die Komponente initialisiert wird.
/// Startet den SMS-Countdown-Timer falls nötig.
///
/// OnInitialized (nicht Async) reicht hier, weil wir keinen
/// await brauchen — der Timer läuft über einen Callback.
/// </summary>
protected override void OnInitialized()
{
if (TfaType == "sms" && TfaExpiration is not null)
{
// Restzeit berechnen
_remainingTime = TfaExpiration.Value - DateTime.Now;
// Timer: Jede Sekunde _remainingTime aktualisieren.
// InvokeAsync + StateHasChanged sagt Blazor: "Zeichne die UI neu".
// Das ist das Blazor-Äquivalent zu setInterval + DOM-Update in JavaScript.
_timer = new System.Threading.Timer(_ =>
{
_remainingTime = TfaExpiration.Value - DateTime.Now;
if (_remainingTime <= TimeSpan.Zero)
{
_remainingTime = TimeSpan.Zero;
_timer?.Dispose();
}
// InvokeAsync ist nötig, weil der Timer auf einem anderen Thread läuft.
// Blazor erlaubt UI-Updates nur auf dem UI-Thread.
InvokeAsync(StateHasChanged);
},
state: null,
dueTime: TimeSpan.Zero, // Sofort starten
period: TimeSpan.FromSeconds(1) // Jede Sekunde
);
}
}
private async Task Submit()
{
_isSubmitting = true;
await OnSubmit.InvokeAsync((_model.Code, TfaType));
_isSubmitting = false;
}
/// <summary>
/// Aufräumen: Timer stoppen wenn die Komponente entfernt wird.
/// Ohne Dispose würde der Timer weiterlaufen und Fehler verursachen,
/// weil er versucht eine nicht mehr existierende UI zu aktualisieren.
/// </summary>
public void Dispose()
{
_timer?.Dispose();
}
// ── Validierungs-Model ──
private class TfaCodeModel
{
[System.ComponentModel.DataAnnotations.Required(ErrorMessage = "Bitte Code eingeben")]
[System.ComponentModel.DataAnnotations.StringLength(6, MinimumLength = 6, ErrorMessage = "Der Code muss 6 Zeichen lang sein")]
public string Code { get; set; } = string.Empty;
}
}

View File

@@ -1,133 +0,0 @@
@* ConfirmDialog: Ersetzt SweetAlert2 aus dem Web-Projekt.
Zeigt einen modalen Bestätigungsdialog mit Titel, Text und Ja/Nein-Buttons.
Wird NICHT über Parameter gesteuert, sondern über eine Methode:
var confirmed = await _dialog.ShowAsync("Titel", "Text");
WARUM eine Methode statt Parameter?
- Ein Dialog ist ein "einmaliges Ereignis", kein dauerhafter Zustand
- Die aufrufende Komponente will auf das Ergebnis WARTEN (await)
- Mit Parametern müsste man den State manuell hin- und herschalten
WARUM kein SweetAlert2?
- SweetAlert2 ist eine JavaScript-Bibliothek
- In Blazor können wir das nativ in C# lösen, ohne JS-Interop
- Weniger Abhängigkeiten = weniger Wartung = weniger Fehlerquellen *@
@if (_isVisible)
{
@* Hintergrund-Overlay: Dunkler Hintergrund hinter dem Dialog.
Im Web-Projekt macht SweetAlert2 das automatisch.
Hier bauen wir es selbst mit CSS. *@
<div class="modal-backdrop fade show"></div>
@* Modal-Dialog: Bootstrap 5 Modal-Markup.
Normalerweise braucht Bootstrap-Modal JavaScript (bootstrap.js)
um zu öffnen/schließen. Wir steuern die Sichtbarkeit stattdessen
über die _isVisible-Variable — Blazor macht das DOM-Update. *@
<div class="modal fade show d-block" tabindex="-1" role="dialog">
<div class="modal-dialog modal-dialog-centered" role="document">
<div class="modal-content">
@* Header: Titel + Schließen-Button *@
<div class="modal-header">
<h5 class="modal-title">@_title</h5>
<button type="button" class="btn-close" @onclick="Cancel"></button>
</div>
@* Body: Beschreibungstext *@
<div class="modal-body">
<p>@_message</p>
</div>
@* Footer: Abbrechen + Bestätigen *@
<div class="modal-footer">
<button type="button" class="btn btn-secondary" @onclick="Cancel">
@_cancelText
</button>
<button type="button" class="btn btn-@_confirmColor" @onclick="Confirm">
@_confirmText
</button>
</div>
</div>
</div>
</div>
}
@code {
// ── Interner State (NICHT Parameter — wird über ShowAsync gesetzt) ──
private bool _isVisible;
private string _title = string.Empty;
private string _message = string.Empty;
private string _confirmText = "Ja";
private string _cancelText = "Abbrechen";
private string _confirmColor = "primary";
/// <summary>
/// TaskCompletionSource: Das Herzstück dieser Komponente.
///
/// WAS IST DAS?
/// Ein TaskCompletionSource erstellt einen Task, der erst dann
/// "fertig" wird, wenn jemand SetResult() aufruft.
///
/// WIE FUNKTIONIERT ES?
/// 1. ShowAsync() erstellt ein neues TaskCompletionSource
/// 2. ShowAsync() gibt dessen Task zurück → der Aufrufer wartet (await)
/// 3. Der Benutzer klickt "Ja" → Confirm() ruft SetResult(true) auf
/// 4. Der await in der aufrufenden Komponente kommt zurück mit true
///
/// Das ist wie ein "Promise" in JavaScript, nur in C#.
/// </summary>
private TaskCompletionSource<bool>? _tcs;
/// <summary>
/// Zeigt den Dialog und wartet auf die Benutzer-Entscheidung.
///
/// Beispiel-Aufruf:
/// var confirmed = await _dialog.ShowAsync(
/// "Unterschreiben",
/// "Möchten Sie das Dokument wirklich unterschreiben?");
/// </summary>
/// <param name="title">Dialog-Überschrift</param>
/// <param name="message">Beschreibungstext</param>
/// <param name="confirmText">Text auf dem Bestätigen-Button (Standard: "Ja")</param>
/// <param name="cancelText">Text auf dem Abbrechen-Button (Standard: "Abbrechen")</param>
/// <param name="confirmColor">Bootstrap-Farbe des Bestätigen-Buttons (Standard: "primary")</param>
/// <returns>true wenn bestätigt, false wenn abgebrochen</returns>
public Task<bool> ShowAsync(
string title,
string message,
string confirmText = "Ja",
string cancelText = "Abbrechen",
string confirmColor = "primary")
{
_title = title;
_message = message;
_confirmText = confirmText;
_cancelText = cancelText;
_confirmColor = confirmColor;
_tcs = new TaskCompletionSource<bool>();
_isVisible = true;
StateHasChanged();
return _tcs.Task;
}
private void Confirm()
{
_isVisible = false;
_tcs?.TrySetResult(true);
StateHasChanged();
}
private void Cancel()
{
_isVisible = false;
_tcs?.TrySetResult(false);
StateHasChanged();
}
}

View File

@@ -1,19 +0,0 @@
<div class="text-center py-5">
@if (!string.IsNullOrEmpty(Icon))
{
<div class="mb-3">
<i class="bi bi-@Icon" style="font-size: 3rem;"></i>
</div>
}
<h2>@Title</h2>
@if (!string.IsNullOrEmpty(Message))
{
<p class="text-muted">@Message</p>
}
</div>
@code {
[Parameter] public string Title { get; set; } = "Fehler";
[Parameter] public string? Message { get; set; }
[Parameter] public string? Icon { get; set; }
}

View File

@@ -1,18 +0,0 @@
<div class="d-flex justify-content-center align-items-center @(Small ? "" : "py-5")" style="@(Small ? "" : "min-height: 40vh;")">
<div class="text-center">
<div class="spinner-border @(Small ? "spinner-border-sm" : "text-primary")"
style="@(Small ? "" : "width: 3rem; height: 3rem;")"
role="status">
<span class="visually-hidden">Laden...</span>
</div>
@if (!Small && Message is not null)
{
<p class="mt-3 text-muted">@Message</p>
}
</div>
</div>
@code {
[Parameter] public bool Small { get; set; }
[Parameter] public string? Message { get; set; }
}

View File

@@ -1,79 +0,0 @@
@* StatusPage: Wiederverwendbare Status-Seite für alle Endzustände.
Ersetzt EnvelopeSigned.cshtml, EnvelopeRejected.cshtml, Error-Views und Expired.
"Dumme" Komponente: Keine Services, keine API-Calls, nur Parameter → Anzeige. *@
<div class="page">
<header class="text-center">
@switch (Type)
{
case "signed":
<div class="status-icon signed">
<i class="bi bi-check-circle"></i>
</div>
<h1>Dokument erfolgreich unterschrieben</h1>
<p class="text-muted">
Sie erhalten eine Bestätigung per E-Mail, sobald alle Empfänger unterschrieben haben.
</p>
break;
case "rejected":
<div class="status-icon rejected">
<i class="bi bi-x-circle"></i>
</div>
<h1>Dokument wurde abgelehnt</h1>
<p class="text-muted">
@if (!string.IsNullOrEmpty(Title) && !string.IsNullOrEmpty(SenderEmail))
{
<span>
Das Dokument «@Title» wurde abgelehnt.
Bei Fragen wenden Sie sich an
<a href="mailto:@SenderEmail">@SenderEmail</a>.
</span>
}
else
{
<span>Dieses Dokument wurde von einem Empfänger abgelehnt.</span>
}
</p>
break;
case "not_found":
<div class="status-icon locked">
<i class="bi bi-question-circle"></i>
</div>
<h1>Dokument nicht gefunden</h1>
<p class="text-muted">
Dieses Dokument existiert nicht oder ist nicht mehr verfügbar.
Wenn Sie diese URL per E-Mail erhalten haben, wenden Sie sich bitte an das IT-Team.
</p>
break;
case "expired":
<div class="status-icon locked">
<i class="bi bi-clock-history"></i>
</div>
<h1>Link abgelaufen</h1>
<p class="text-muted">
Der Zugang zu diesem Dokument ist abgelaufen.
</p>
break;
}
</header>
</div>
@code {
/// <summary>
/// Bestimmt welche Status-Variante angezeigt wird.
/// Erlaubte Werte: "signed", "rejected", "not_found", "expired"
/// </summary>
[Parameter, EditorRequired]
public string Type { get; set; } = string.Empty;
/// <summary>E-Mail des Absenders — nur bei "rejected" relevant.</summary>
[Parameter]
public string? SenderEmail { get; set; }
/// <summary>Titel des Umschlags — nur bei "rejected" relevant.</summary>
[Parameter]
public string? Title { get; set; }
}

View File

@@ -1,70 +0,0 @@
@* Toast: Zeigt kurze Benachrichtigungen an. Ersetzt AlertifyJS aus dem Web-Projekt.
Wird einmal im MainLayout platziert. Hört auf den ToastService.
Wenn eine Komponente irgendwo _toast.ShowSuccess("Text") aufruft,
erscheint hier ein Toast und verschwindet nach 4 Sekunden automatisch.
Bootstrap Toast-Klassen werden genutzt:
- toast-container: Positionierung (oben rechts)
- toast: Einzelner Toast
- bg-success/bg-danger/etc: Farbe basierend auf Typ *@
@inject ToastService ToastService
@implements IDisposable
@if (ToastService.Messages.Count > 0)
{
@* toast-container: Bootstrap-Klasse die Toasts oben rechts positioniert.
position-fixed: Bleibt an der Stelle auch beim Scrollen.
z-index 1080: Über dem Modal-Backdrop (1070) damit Toasts
auch während eines ConfirmDialogs sichtbar sind. *@
<div class="toast-container position-fixed top-0 end-0 p-3" style="z-index: 1080;">
@foreach (var message in ToastService.Messages)
{
<div class="toast show align-items-center text-white bg-@message.Type border-0"
role="alert">
<div class="d-flex">
<div class="toast-body">
<i class="bi @message.IconClass me-2"></i>
@message.Text
</div>
<button type="button"
class="btn-close btn-close-white me-2 m-auto"
@onclick="() => ToastService.Dismiss(message)">
</button>
</div>
</div>
}
</div>
}
@code {
protected override void OnInitialized()
{
// Subscriber: Wenn der ToastService eine Änderung meldet,
// zeichnet diese Komponente sich neu (StateHasChanged).
// So erscheinen/verschwinden Toasts automatisch.
ToastService.OnChange += HandleChange;
}
/// <summary>
/// InvokeAsync ist nötig weil OnChange von einem
/// async void (dem Timer im ToastService) ausgelöst wird.
/// Das kann auf einem anderen Thread passieren.
/// InvokeAsync wechselt zurück auf den Blazor UI-Thread.
/// </summary>
private async void HandleChange()
{
await InvokeAsync(StateHasChanged);
}
/// <summary>
/// Event-Handler abmelden wenn die Komponente entfernt wird.
/// Ohne das würde der ToastService eine Referenz auf eine
/// nicht mehr existierende Komponente halten → Memory Leak.
/// </summary>
public void Dispose()
{
ToastService.OnChange -= HandleChange;
}
}

View File

@@ -1,17 +0,0 @@
<Project Sdk="Microsoft.NET.Sdk.BlazorWebAssembly">
<PropertyGroup>
<TargetFramework>net9.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
<NoDefaultLaunchSettingsFile>true</NoDefaultLaunchSettingsFile>
<StaticWebAssetProjectMode>Default</StaticWebAssetProjectMode>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Microsoft.AspNetCore.Components.Authorization" Version="9.0.3" />
<PackageReference Include="Microsoft.AspNetCore.Components.WebAssembly" Version="9.0.3" />
<PackageReference Include="Microsoft.Extensions.Http" Version="9.0.3" />
</ItemGroup>
</Project>

View File

@@ -1,13 +0,0 @@
namespace EnvelopeGenerator.ReceiverUI.Client.Models;
/// <summary>
/// Hält den aktuellen Authentifizierungs-Zustand im Client.
/// Wird vom ApiAuthStateProvider gesetzt und von Komponenten gelesen.
/// </summary>
public class AuthState
{
public bool IsAuthenticated { get; set; }
public string? Role { get; set; }
public string? EnvelopeUuid { get; set; }
public string? ReceiverEmail { get; set; }
}

View File

@@ -1,12 +0,0 @@
namespace EnvelopeGenerator.ReceiverUI.Client.Models;
/// <summary>
/// Client-seitiges DTO für Dokument-Daten.
/// </summary>
public record DocumentModel
{
public int Id { get; init; }
public int EnvelopeId { get; init; }
public DateTime AddedWhen { get; init; }
public byte[]? ByteData { get; init; }
}

View File

@@ -1,27 +0,0 @@
namespace EnvelopeGenerator.ReceiverUI.Client.Models;
/// <summary>
/// Client-seitiges DTO für Umschlag-Daten.
/// Muss nur die JSON-Properties matchen, die die API zurückgibt
/// und die der Client tatsächlich braucht.
///
/// WARUM eigene DTOs statt die aus EnvelopeGenerator.Application?
/// - Application hat Server-Abhängigkeiten (SqlClient, JwtBearer, EF Core)
/// - Diese Pakete existieren nicht für browser-wasm → Build-Fehler
/// - Der Client braucht nur eine Teilmenge der Felder
/// - Eigene DTOs machen den Client unabhängig vom Server
/// </summary>
public record EnvelopeModel
{
public int Id { get; init; }
public string Uuid { get; init; } = string.Empty;
public string Title { get; init; } = string.Empty;
public string Message { get; init; } = string.Empty;
public bool UseAccessCode { get; init; }
public bool TFAEnabled { get; init; }
public bool ReadOnly { get; init; }
public string Language { get; init; } = "de-DE";
public DateTime AddedWhen { get; init; }
public UserModel? User { get; init; }
public IEnumerable<DocumentModel>? Documents { get; init; }
}

View File

@@ -1,15 +0,0 @@
namespace EnvelopeGenerator.ReceiverUI.Client.Models;
/// <summary>
/// Client-seitiges DTO für die Envelope-Receiver-Zuordnung.
/// </summary>
public record EnvelopeReceiverModel
{
public EnvelopeModel? Envelope { get; init; }
public ReceiverModel? Receiver { get; init; }
public int EnvelopeId { get; init; }
public int ReceiverId { get; init; }
public int Sequence { get; init; }
public string? Name { get; init; }
public bool HasPhoneNumber { get; init; }
}

View File

@@ -1,6 +0,0 @@
namespace EnvelopeGenerator.ReceiverUI.Client.Models
{
public class EnvelopeViewModel
{
}
}

View File

@@ -1,31 +0,0 @@
namespace EnvelopeGenerator.ReceiverUI.Client.Models;
/// <summary>
/// Client-seitiges DTO für die Antwort des ReceiverAuthControllers.
/// Wird 1:1 aus dem JSON deserialisiert.
///
/// WARUM ein eigenes Client-Model statt das API-Model zu referenzieren?
/// - Das API-Projekt hat Server-Abhängigkeiten (EF Core, SqlClient, etc.)
/// - Diese Pakete existieren nicht für browser-wasm → Build-Fehler
/// - Die Property-Namen müssen nur zum JSON passen (case-insensitive)
/// </summary>
public record ReceiverAuthModel
{
/// <summary>
/// Aktueller Status des Empfänger-Flows.
/// Werte: "requires_access_code", "requires_tfa", "show_document",
/// "already_signed", "rejected", "not_found", "expired", "error"
/// </summary>
public string Status { get; init; } = string.Empty;
public string? Title { get; init; }
public string? Message { get; init; }
public string? SenderEmail { get; init; }
public string? ReceiverName { get; init; }
public bool TfaEnabled { get; init; }
public bool HasPhoneNumber { get; init; }
public bool ReadOnly { get; init; }
public string? TfaType { get; init; }
public DateTime? TfaExpiration { get; init; }
public string? ErrorMessage { get; init; }
}

View File

@@ -1,13 +0,0 @@
namespace EnvelopeGenerator.ReceiverUI.Client.Models;
/// <summary>
/// Client-seitiges DTO für Empfänger-Daten.
/// </summary>
public record ReceiverModel
{
public int Id { get; init; }
public string EmailAddress { get; init; } = string.Empty;
public string Signature { get; init; } = string.Empty;
public DateTime AddedWhen { get; init; }
public DateTime? TfaRegDeadline { get; init; }
}

View File

@@ -1,10 +0,0 @@
namespace EnvelopeGenerator.ReceiverUI.Client.Models;
/// <summary>
/// Client-seitiges DTO für Benutzer-Daten (Absender).
/// </summary>
public record UserModel
{
public string? Email { get; init; }
public string? DisplayName { get; init; }
}

View File

@@ -1,159 +0,0 @@
@page "/envelope/{EnvelopeKey}"
@rendermode InteractiveAuto
@inject IReceiverAuthService ReceiverAuthService
@inject EnvelopeState State
@implements IDisposable
<PageTitle>Dokument</PageTitle>
@switch (State.Status)
{
case EnvelopePageStatus.Loading:
<LoadingIndicator Message="Dokument wird geladen..." />
break;
case EnvelopePageStatus.NotFound:
<StatusPage Type="not_found" />
break;
case EnvelopePageStatus.AlreadySigned:
<StatusPage Type="signed"
Title="@State.Title"
SenderEmail="@State.SenderEmail" />
break;
case EnvelopePageStatus.Rejected:
<StatusPage Type="rejected"
Title="@State.Title"
SenderEmail="@State.SenderEmail" />
break;
case EnvelopePageStatus.Expired:
<StatusPage Type="expired" />
break;
case EnvelopePageStatus.RequiresAccessCode:
<AccessCodeForm EnvelopeKey="@EnvelopeKey"
ErrorMessage="@State.ErrorMessage"
SenderEmail="@State.SenderEmail"
Title="@State.Title"
TfaEnabled="@State.TfaEnabled"
HasPhoneNumber="@State.HasPhoneNumber"
OnSubmit="HandleAccessCodeSubmit" />
break;
case EnvelopePageStatus.RequiresTwoFactor:
<TfaForm EnvelopeKey="@EnvelopeKey"
TfaType="@(State.TfaType ?? "authenticator")"
TfaExpiration="@State.TfaExpiration"
HasPhoneNumber="@State.HasPhoneNumber"
ErrorMessage="@State.ErrorMessage"
OnSubmit="HandleTfaSubmit" />
break;
case EnvelopePageStatus.ShowDocument:
@* Phase 4 (PSPDFKit) kommt später — vorerst Platzhalter *@
<div class="text-center mt-5">
<div class="status-icon signed">
<i class="bi bi-file-earmark-check"></i>
</div>
<h2>Dokument bereit</h2>
<p class="text-muted">
«@State.Title» — PDF-Viewer wird in Phase 4 integriert.
</p>
@if (State.ReadOnly)
{
<span class="badge bg-secondary">Nur Lesen</span>
}
</div>
break;
case EnvelopePageStatus.Error:
<ErrorDisplay Title="Fehler" Message="@State.ErrorMessage" />
break;
}
@code {
[Parameter] public string EnvelopeKey { get; set; } = default!;
protected override async Task OnInitializedAsync()
{
State.OnChange += StateHasChanged;
await LoadStatusAsync();
}
/// <summary>
/// Erster API-Call: Status prüfen.
/// Entspricht dem GET /Envelope/{key} im Web-Projekt.
/// Die API entscheidet, was passiert (AccessCode nötig? Bereits signiert? etc.)
/// </summary>
private async Task LoadStatusAsync()
{
State.SetLoading();
var result = await ReceiverAuthService.GetStatusAsync(EnvelopeKey);
if (result.IsSuccess && result.Data is not null)
{
State.ApplyApiResponse(result.Data);
}
else if (result.StatusCode == 404)
{
State.SetNotFound();
}
else
{
State.SetError(result.ErrorMessage ?? "Verbindung zum Server fehlgeschlagen.");
}
}
/// <summary>
/// Zweiter API-Call: AccessCode senden.
/// Wird von AccessCodeForm aufgerufen (OnSubmit-Callback).
/// Die API prüft den Code und antwortet mit dem nächsten Status.
/// </summary>
private async Task HandleAccessCodeSubmit((string Code, bool PreferSms) submission)
{
var result = await ReceiverAuthService.SubmitAccessCodeAsync(
EnvelopeKey, submission.Code, submission.PreferSms);
if (result.IsSuccess && result.Data is not null)
{
State.ApplyApiResponse(result.Data);
}
else if (result.Data is not null)
{
// 401 mit Body → falscher Code, API gibt trotzdem ReceiverAuthModel zurück
State.ApplyApiResponse(result.Data);
}
else
{
State.SetError(result.ErrorMessage ?? "Fehler bei der Code-Prüfung.");
}
}
/// <summary>
/// Dritter API-Call: TFA-Code senden.
/// Wird von TfaForm aufgerufen (OnSubmit-Callback).
/// </summary>
private async Task HandleTfaSubmit((string Code, string Type) submission)
{
var result = await ReceiverAuthService.SubmitTfaCodeAsync(
EnvelopeKey, submission.Code, submission.Type);
if (result.IsSuccess && result.Data is not null)
{
State.ApplyApiResponse(result.Data);
}
else if (result.Data is not null)
{
State.ApplyApiResponse(result.Data);
}
else
{
State.SetError(result.ErrorMessage ?? "Fehler bei der TFA-Prüfung.");
}
}
public void Dispose() => State.OnChange -= StateHasChanged;
}

View File

@@ -1,5 +0,0 @@
<h3>Home</h3>
@code {
}

View File

@@ -1,5 +0,0 @@
<h3>NotFound</h3>
@code {
}

View File

@@ -1,30 +0,0 @@
using Microsoft.AspNetCore.Components.Authorization;
using Microsoft.AspNetCore.Components.WebAssembly.Hosting;
using EnvelopeGenerator.ReceiverUI.Client.Auth;
using EnvelopeGenerator.ReceiverUI.Client.Services;
using EnvelopeGenerator.ReceiverUI.Client.State;
var builder = WebAssemblyHostBuilder.CreateDefault(args);
// HttpClient: BaseAddress zeigt auf den ReceiverUI-Server (gleiche Domain)
// Von dort werden alle /api/* Calls via YARP an die echte API weitergeleitet
builder.Services.AddScoped(sp =>
new HttpClient { BaseAddress = new Uri(builder.HostEnvironment.BaseAddress) });
// Auth: Blazor fragt über diesen Provider "Ist der Nutzer eingeloggt?"
builder.Services.AddAuthorizationCore();
builder.Services.AddScoped<ApiAuthStateProvider>();
builder.Services.AddScoped<AuthenticationStateProvider>(sp =>
sp.GetRequiredService<ApiAuthStateProvider>());
// API-Services: Je ein Service pro API-Controller
builder.Services.AddScoped<IAuthService, AuthService>();
builder.Services.AddScoped<IEnvelopeService, EnvelopeService>();
builder.Services.AddScoped<IReceiverAuthService, ReceiverAuthService>();
// State: Ein State-Objekt pro Browser-Tab
builder.Services.AddScoped<EnvelopeState>();
builder.Services.AddScoped<ToastService>();
await builder.Build().RunAsync();

View File

@@ -1,54 +0,0 @@
using EnvelopeGenerator.ReceiverUI.Client.Services.Base;
namespace EnvelopeGenerator.ReceiverUI.Client.Services;
/// <summary>
/// Spricht mit dem bestehenden AuthController der API.
/// Die API erkennt den Nutzer über das Cookie "AuthToken" automatisch.
/// </summary>
public class AuthService : ApiServiceBase, IAuthService
{
public AuthService(HttpClient http, ILogger<AuthService> logger) : base(http, logger) { }
public async Task<ApiResponse> CheckAuthAsync(string? role = null, CancellationToken ct = default)
{
var endpoint = role is not null ? $"api/auth/check?role={role}" : "api/auth/check";
try
{
var response = await Http.GetAsync(endpoint, ct);
return response.IsSuccessStatusCode
? ApiResponse.Success((int)response.StatusCode)
: ApiResponse.Failure((int)response.StatusCode);
}
catch (HttpRequestException ex)
{
Logger.LogError(ex, "HTTP error calling GET {Endpoint}", endpoint);
return ApiResponse.Failure(0, "Verbindung zum Server fehlgeschlagen.");
}
catch (TaskCanceledException)
{
return ApiResponse.Failure(0, "Anfrage abgebrochen.");
}
}
public async Task<ApiResponse> LogoutAsync(CancellationToken ct = default)
{
const string endpoint = "api/auth/logout";
try
{
var response = await Http.PostAsync(endpoint, null, ct);
return response.IsSuccessStatusCode
? ApiResponse.Success((int)response.StatusCode)
: ApiResponse.Failure((int)response.StatusCode);
}
catch (HttpRequestException ex)
{
Logger.LogError(ex, "HTTP error calling POST {Endpoint}", endpoint);
return ApiResponse.Failure(0, "Verbindung zum Server fehlgeschlagen.");
}
catch (TaskCanceledException)
{
return ApiResponse.Failure(0, "Anfrage abgebrochen.");
}
}
}

View File

@@ -1,46 +0,0 @@
namespace EnvelopeGenerator.ReceiverUI.Client.Services.Base;
/// <summary>
/// Einheitliches Response-Objekt für ALLE API-Aufrufe.
///
/// WARUM: Jeder API-Aufruf kann fehlschlagen (Netzwerk, 401, 500...).
/// Statt überall try-catch zu haben, kapselt dieses Objekt Erfolg/Fehler einheitlich.
/// So kann jede Blazor-Komponente einheitlich darauf reagieren.
/// </summary>
public record ApiResponse<T>
{
public bool IsSuccess { get; init; }
public T? Data { get; init; }
public int StatusCode { get; init; }
public string? ErrorMessage { get; init; }
public static ApiResponse<T> Success(T data, int statusCode = 200)
=> new() { IsSuccess = true, Data = data, StatusCode = statusCode };
public static ApiResponse<T> Failure(int statusCode, string? error = null)
=> new() { IsSuccess = false, StatusCode = statusCode, ErrorMessage = error };
/// <summary>
/// Failure mit deserialisiertem Body — für Fälle wo die API
/// bei 401/404 trotzdem ein strukturiertes JSON zurückgibt
/// (z.B. ReceiverAuthResponse mit ErrorMessage + Status).
/// </summary>
public static ApiResponse<T> Failure(int statusCode, string? error, T? data)
=> new() { IsSuccess = false, StatusCode = statusCode, ErrorMessage = error, Data = data };
}
/// <summary>
/// Response ohne Daten (für POST/PUT/DELETE die nur Status zurückgeben).
/// </summary>
public record ApiResponse
{
public bool IsSuccess { get; init; }
public int StatusCode { get; init; }
public string? ErrorMessage { get; init; }
public static ApiResponse Success(int statusCode = 200)
=> new() { IsSuccess = true, StatusCode = statusCode };
public static ApiResponse Failure(int statusCode, string? error = null)
=> new() { IsSuccess = false, StatusCode = statusCode, ErrorMessage = error };
}

View File

@@ -1,141 +0,0 @@
using System.Net.Http.Json;
using Microsoft.Extensions.Logging;
namespace EnvelopeGenerator.ReceiverUI.Client.Services.Base;
/// <summary>
/// Basisklasse für ALLE API-Services.
///
/// WARUM eine Basisklasse?
/// - Einheitliches Error-Handling: Jeder API-Aufruf wird gleich behandelt
/// - DRY (Don't Repeat Yourself): Logging, Fehlerbehandlung, Serialisierung nur einmal
/// - Einfache Erweiterung: Retry-Logik, Token-Refresh etc. nur hier ändern
/// </summary>
public abstract class ApiServiceBase
{
protected readonly HttpClient Http;
protected readonly ILogger Logger;
protected ApiServiceBase(HttpClient http, ILogger logger)
{
Http = http;
Logger = logger;
}
/// <summary>
/// GET-Request mit Deserialisierung.
/// Alle API GET-Aufrufe gehen durch diese Methode.
/// </summary>
protected async Task<ApiResponse<T>> GetAsync<T>(string endpoint, CancellationToken ct = default)
{
try
{
var response = await Http.GetAsync(endpoint, ct);
if (!response.IsSuccessStatusCode)
{
var errorBody = await response.Content.ReadAsStringAsync(ct);
Logger.LogWarning("GET {Endpoint} failed: {Status} - {Body}",
endpoint, (int)response.StatusCode, errorBody);
// Versuche den Body trotzdem zu deserialisieren —
// die API gibt bei 401/404 oft strukturierte JSON-Antworten zurück
// (z.B. ReceiverAuthResponse mit ErrorMessage + Status)
var errorData = await TryDeserializeAsync<T>(response, ct);
return ApiResponse<T>.Failure((int)response.StatusCode, errorBody, errorData);
}
var data = await response.Content.ReadFromJsonAsync<T>(cancellationToken: ct);
return ApiResponse<T>.Success(data!, (int)response.StatusCode);
}
catch (HttpRequestException ex)
{
Logger.LogError(ex, "HTTP error calling GET {Endpoint}", endpoint);
return ApiResponse<T>.Failure(0, "Verbindung zum Server fehlgeschlagen.");
}
catch (TaskCanceledException)
{
Logger.LogWarning("GET {Endpoint} was cancelled", endpoint);
return ApiResponse<T>.Failure(0, "Anfrage abgebrochen.");
}
}
/// <summary>
/// POST-Request mit Body und Response-Deserialisierung.
/// </summary>
protected async Task<ApiResponse<TResponse>> PostAsync<TRequest, TResponse>(
string endpoint, TRequest body, CancellationToken ct = default)
{
try
{
var response = await Http.PostAsJsonAsync(endpoint, body, ct);
if (!response.IsSuccessStatusCode)
{
var errorBody = await response.Content.ReadAsStringAsync(ct);
Logger.LogWarning("POST {Endpoint} failed: {Status} - {Body}",
endpoint, (int)response.StatusCode, errorBody);
var errorData = await TryDeserializeAsync<TResponse>(response, ct);
return ApiResponse<TResponse>.Failure((int)response.StatusCode, errorBody, errorData);
}
var data = await response.Content.ReadFromJsonAsync<TResponse>(cancellationToken: ct);
return ApiResponse<TResponse>.Success(data!, (int)response.StatusCode);
}
catch (HttpRequestException ex)
{
Logger.LogError(ex, "HTTP error calling POST {Endpoint}", endpoint);
return ApiResponse<TResponse>.Failure(0, "Verbindung zum Server fehlgeschlagen.");
}
}
/// <summary>
/// POST-Request ohne Response-Body (z.B. Logout).
/// </summary>
protected async Task<ApiResponse> PostAsync<TRequest>(
string endpoint, TRequest body, CancellationToken ct = default)
{
try
{
var response = await Http.PostAsJsonAsync(endpoint, body, ct);
if (!response.IsSuccessStatusCode)
{
var errorBody = await response.Content.ReadAsStringAsync(ct);
return ApiResponse.Failure((int)response.StatusCode, errorBody);
}
return ApiResponse.Success((int)response.StatusCode);
}
catch (HttpRequestException ex)
{
Logger.LogError(ex, "HTTP error calling POST {Endpoint}", endpoint);
return ApiResponse.Failure(0, "Verbindung zum Server fehlgeschlagen.");
}
}
/// <summary>
/// Versucht den Response-Body als JSON zu deserialisieren.
/// Gibt null zurück wenn es nicht klappt (z.B. bei HTML-Fehlerseiten).
/// </summary>
private static async Task<T?> TryDeserializeAsync<T>(HttpResponseMessage response, CancellationToken ct)
{
try
{
// Nur versuchen wenn der Content-Type JSON ist
if (response.Content.Headers.ContentType?.MediaType == "application/json")
{
return await response.Content.ReadFromJsonAsync<T>(cancellationToken: ct);
}
}
catch
{
// Ignorieren — der Body war kein valides JSON
}
return default;
}
}

View File

@@ -1,16 +0,0 @@
using EnvelopeGenerator.ReceiverUI.Client.Models;
using EnvelopeGenerator.ReceiverUI.Client.Services.Base;
namespace EnvelopeGenerator.ReceiverUI.Client.Services;
public class EnvelopeService : ApiServiceBase, IEnvelopeService
{
public EnvelopeService(HttpClient http, ILogger<EnvelopeService> logger) : base(http, logger) { }
public Task<ApiResponse<IEnumerable<EnvelopeModel>>> GetEnvelopesAsync(CancellationToken ct = default)
=> GetAsync<IEnumerable<EnvelopeModel>>("api/envelope", ct);
public Task<ApiResponse<IEnumerable<EnvelopeReceiverModel>>> GetEnvelopeReceiversAsync(
CancellationToken ct = default)
=> GetAsync<IEnumerable<EnvelopeReceiverModel>>("api/envelopereceiver", ct);
}

View File

@@ -1,6 +0,0 @@
namespace EnvelopeGenerator.ReceiverUI.Client.Services
{
public class HistoryService
{
}
}

View File

@@ -1,20 +0,0 @@
using EnvelopeGenerator.ReceiverUI.Client.Services.Base;
namespace EnvelopeGenerator.ReceiverUI.Client.Services;
/// <summary>
/// Kommuniziert mit dem AuthController der API.
///
/// WARUM Interface + Implementierung?
/// - Testbarkeit: In Unit-Tests kann man einen Mock verwenden
/// - Austauschbarkeit: Wenn sich die API ändert, ändert sich nur die Implementierung
/// - Blazor-Konvention: Services werden über Interfaces per DI registriert
/// </summary>
public interface IAuthService
{
/// <summary>Prüft ob der Nutzer eingeloggt ist → GET /api/auth/check</summary>
Task<ApiResponse> CheckAuthAsync(string? role = null, CancellationToken ct = default);
/// <summary>Logout → POST /api/auth/logout</summary>
Task<ApiResponse> LogoutAsync(CancellationToken ct = default);
}

View File

@@ -1,18 +0,0 @@
using EnvelopeGenerator.ReceiverUI.Client.Models;
using EnvelopeGenerator.ReceiverUI.Client.Services.Base;
namespace EnvelopeGenerator.ReceiverUI.Client.Services;
/// <summary>
/// Kommuniziert mit EnvelopeController und EnvelopeReceiverController.
/// Verwendet Client-eigene Models statt der Server-DTOs.
/// </summary>
public interface IEnvelopeService
{
/// <summary>Lädt Umschläge → GET /api/envelope</summary>
Task<ApiResponse<IEnumerable<EnvelopeModel>>> GetEnvelopesAsync(CancellationToken ct = default);
/// <summary>Lädt EnvelopeReceiver → GET /api/envelopereceiver</summary>
Task<ApiResponse<IEnumerable<EnvelopeReceiverModel>>> GetEnvelopeReceiversAsync(
CancellationToken ct = default);
}

View File

@@ -1,6 +0,0 @@
namespace EnvelopeGenerator.ReceiverUI.Client.Services
{
public interface IHistoryService
{
}
}

View File

@@ -1,26 +0,0 @@
using EnvelopeGenerator.ReceiverUI.Client.Models;
using EnvelopeGenerator.ReceiverUI.Client.Services.Base;
namespace EnvelopeGenerator.ReceiverUI.Client.Services;
/// <summary>
/// Kommuniziert mit dem ReceiverAuthController der API.
///
/// Drei Methoden — eine pro Endpunkt:
/// 1. GetStatusAsync → GET /api/receiverauth/{key}/status
/// 2. SubmitAccessCodeAsync → POST /api/receiverauth/{key}/access-code
/// 3. SubmitTfaCodeAsync → POST /api/receiverauth/{key}/tfa
/// </summary>
public interface IReceiverAuthService
{
/// <summary>Prüft den aktuellen Status des Empfänger-Flows</summary>
Task<ApiResponse<ReceiverAuthModel>> GetStatusAsync(string key, CancellationToken ct = default);
/// <summary>Sendet den Zugangscode zur Prüfung</summary>
Task<ApiResponse<ReceiverAuthModel>> SubmitAccessCodeAsync(
string key, string accessCode, bool preferSms, CancellationToken ct = default);
/// <summary>Sendet den TFA-Code (SMS oder Authenticator) zur Prüfung</summary>
Task<ApiResponse<ReceiverAuthModel>> SubmitTfaCodeAsync(
string key, string code, string type, CancellationToken ct = default);
}

View File

@@ -1,41 +0,0 @@
using EnvelopeGenerator.ReceiverUI.Client.Models;
using EnvelopeGenerator.ReceiverUI.Client.Services.Base;
namespace EnvelopeGenerator.ReceiverUI.Client.Services;
/// <summary>
/// Spricht mit dem ReceiverAuthController der API.
///
/// Nutzt die Basisklasse ApiServiceBase für einheitliches Error-Handling.
/// Jede Methode gibt ApiResponse&lt;ReceiverAuthModel&gt; zurück —
/// egal ob Erfolg oder Fehler. Die aufrufende Komponente prüft dann
/// result.IsSuccess und result.Data.Status.
///
/// WARUM gibt die API bei 401 trotzdem ein ReceiverAuthModel zurück?
/// Weil auch bei "falscher Code" der Client wissen muss, welchen
/// Status er anzeigen soll (z.B. "requires_access_code" + ErrorMessage).
/// Deshalb deserialisieren wir auch bei Fehler-Statuscodes den Body.
/// </summary>
public class ReceiverAuthService : ApiServiceBase, IReceiverAuthService
{
public ReceiverAuthService(HttpClient http, ILogger<ReceiverAuthService> logger)
: base(http, logger) { }
public Task<ApiResponse<ReceiverAuthModel>> GetStatusAsync(
string key, CancellationToken ct = default)
=> GetAsync<ReceiverAuthModel>($"api/receiverauth/{key}/status", ct);
public Task<ApiResponse<ReceiverAuthModel>> SubmitAccessCodeAsync(
string key, string accessCode, bool preferSms, CancellationToken ct = default)
=> PostAsync<object, ReceiverAuthModel>(
$"api/receiverauth/{key}/access-code",
new { AccessCode = accessCode, PreferSms = preferSms },
ct);
public Task<ApiResponse<ReceiverAuthModel>> SubmitTfaCodeAsync(
string key, string code, string type, CancellationToken ct = default)
=> PostAsync<object, ReceiverAuthModel>(
$"api/receiverauth/{key}/tfa",
new { Code = code, Type = type },
ct);
}

View File

@@ -1,86 +0,0 @@
namespace EnvelopeGenerator.ReceiverUI.Client.Services;
/// <summary>
/// Service für Toast-Benachrichtigungen. Ersetzt AlertifyJS aus dem Web-Projekt.
///
/// WARUM ein Service und keine Komponente mit Parametern?
/// - Toasts können von ÜBERALL ausgelöst werden (Pages, Services, andere Komponenten)
/// - Ein Service ist über Dependency Injection überall verfügbar
/// - Die Toast-Komponente im Layout hört auf diesen Service und rendert die Nachrichten
///
/// PATTERN: Pub/Sub (Publisher/Subscriber)
/// - Publisher: Jede Komponente die _toast.ShowSuccess("...") aufruft
/// - Subscriber: Die Toast-Komponente im MainLayout, die auf OnChange hört
///
/// Das ist das gleiche Pattern wie beim EnvelopeState.
/// </summary>
public class ToastService
{
/// <summary>
/// Liste aller aktuell sichtbaren Toasts.
/// Mehrere Toasts können gleichzeitig angezeigt werden (gestapelt).
/// </summary>
public List<ToastMessage> Messages { get; } = [];
/// <summary>Event: Informiert die Toast-Komponente über Änderungen</summary>
public event Action? OnChange;
public void ShowSuccess(string text) => Show(text, "success");
public void ShowError(string text) => Show(text, "danger");
public void ShowInfo(string text) => Show(text, "info");
public void ShowWarning(string text) => Show(text, "warning");
/// <summary>
/// Fügt einen Toast hinzu und entfernt ihn nach der angegebenen Dauer automatisch.
///
/// WARUM async void?
/// Normalerweise vermeidet man async void. Hier ist es ok, weil:
/// - Es ist ein Fire-and-Forget-Timer (wir warten nicht auf das Ergebnis)
/// - Fehler im Delay können die App nicht zum Absturz bringen
/// - Das ist ein gängiges Pattern für Auto-Dismiss-Logik
/// </summary>
private async void Show(string text, string type, int durationMs = 4000)
{
var message = new ToastMessage(text, type);
Messages.Add(message);
OnChange?.Invoke();
// Nach Ablauf der Dauer automatisch entfernen
await Task.Delay(durationMs);
Messages.Remove(message);
OnChange?.Invoke();
}
/// <summary>Entfernt einen Toast sofort (z.B. wenn der Benutzer auf X klickt)</summary>
public void Dismiss(ToastMessage message)
{
Messages.Remove(message);
OnChange?.Invoke();
}
}
/// <summary>
/// Ein einzelner Toast-Eintrag.
///
/// WARUM ein record statt class?
/// - Records haben automatisch Equals/GetHashCode basierend auf allen Properties
/// - Wir brauchen das für Messages.Remove() — es vergleicht über Referenz-Gleichheit
/// - Die Id (Guid) macht jeden Toast einzigartig, auch bei gleichem Text
/// </summary>
public record ToastMessage(string Text, string Type)
{
/// <summary>Eindeutige Id — damit zwei Toasts mit gleichem Text unterscheidbar sind</summary>
public Guid Id { get; } = Guid.NewGuid();
/// <summary>
/// Gibt die Bootstrap-Icon-Klasse basierend auf dem Typ zurück.
/// success → check-circle, danger → x-circle, etc.
/// </summary>
public string IconClass => Type switch
{
"success" => "bi-check-circle-fill",
"danger" => "bi-x-circle-fill",
"warning" => "bi-exclamation-triangle-fill",
_ => "bi-info-circle-fill"
};
}

View File

@@ -1,6 +0,0 @@
namespace EnvelopeGenerator.ReceiverUI.Client.State
{
public class AuthState
{
}
}

View File

@@ -1,130 +0,0 @@
using EnvelopeGenerator.ReceiverUI.Client.Models;
namespace EnvelopeGenerator.ReceiverUI.Client.State;
/// <summary>
/// Hält den aktuellen Zustand des geladenen Umschlags.
///
/// WARUM ein eigenes State-Objekt?
/// - Mehrere Komponenten auf einer Seite brauchen die gleichen Daten
/// - Ohne State müsste jede Komponente die Daten selbst laden → doppelte API-Calls
/// - StateHasChanged() informiert automatisch alle Subscriber
///
/// PATTERN: "Observable State" — Services setzen den State, Komponenten reagieren darauf.
///
/// Die Set-Methoden nehmen jetzt ein ReceiverAuthModel entgegen,
/// damit alle Felder (Title, SenderEmail, TfaType etc.) zentral gespeichert werden.
/// </summary>
public class EnvelopeState
{
private EnvelopePageStatus _status = EnvelopePageStatus.Loading;
/// <summary>Aktueller Seitenstatus</summary>
public EnvelopePageStatus Status
{
get => _status;
private set
{
_status = value;
NotifyStateChanged();
}
}
// ── Felder aus ReceiverAuthModel ──
/// <summary>Titel des Umschlags (z.B. "Vertragsdokument")</summary>
public string? Title { get; private set; }
/// <summary>Nachricht des Absenders</summary>
public string? Message { get; private set; }
/// <summary>E-Mail des Absenders (für Rückfragen-Hinweis)</summary>
public string? SenderEmail { get; private set; }
/// <summary>Ob TFA für diesen Umschlag aktiviert ist</summary>
public bool TfaEnabled { get; private set; }
/// <summary>Ob der Empfänger eine Telefonnummer hat (für SMS-TFA)</summary>
public bool HasPhoneNumber { get; private set; }
/// <summary>Ob das Dokument nur gelesen werden soll (ReadAndConfirm)</summary>
public bool ReadOnly { get; private set; }
/// <summary>TFA-Typ: "sms" oder "authenticator"</summary>
public string? TfaType { get; private set; }
/// <summary>Ablaufzeit des SMS-Codes (für Countdown-Timer)</summary>
public DateTime? TfaExpiration { get; private set; }
/// <summary>Fehlermeldung (z.B. "Falscher Zugangscode")</summary>
public string? ErrorMessage { get; private set; }
// ── Zustandsübergänge ──
public void SetLoading()
{
ErrorMessage = null;
Status = EnvelopePageStatus.Loading;
}
/// <summary>
/// Setzt den State aus einer API-Antwort.
/// Zentrale Methode — alle Endpunkte liefern ReceiverAuthModel,
/// und diese Methode mappt den Status-String auf das richtige Enum.
/// </summary>
public void ApplyApiResponse(ReceiverAuthModel model)
{
// Gemeinsame Felder immer übernehmen
Title = model.Title ?? Title;
Message = model.Message ?? Message;
SenderEmail = model.SenderEmail ?? SenderEmail;
TfaEnabled = model.TfaEnabled;
HasPhoneNumber = model.HasPhoneNumber;
ReadOnly = model.ReadOnly;
TfaType = model.TfaType ?? TfaType;
TfaExpiration = model.TfaExpiration ?? TfaExpiration;
ErrorMessage = model.ErrorMessage;
// Status-String → Enum
Status = model.Status switch
{
"requires_access_code" => EnvelopePageStatus.RequiresAccessCode,
"requires_tfa" => EnvelopePageStatus.RequiresTwoFactor,
"show_document" => EnvelopePageStatus.ShowDocument,
"already_signed" => EnvelopePageStatus.AlreadySigned,
"rejected" => EnvelopePageStatus.Rejected,
"not_found" => EnvelopePageStatus.NotFound,
"expired" => EnvelopePageStatus.Expired,
"error" => EnvelopePageStatus.Error,
_ => EnvelopePageStatus.Error
};
}
/// <summary>Setzt Fehler wenn der API-Call selbst fehlschlägt (Netzwerk etc.)</summary>
public void SetError(string message)
{
ErrorMessage = message;
Status = EnvelopePageStatus.Error;
}
/// <summary>Setzt NotFound (z.B. bei 404 ohne Body)</summary>
public void SetNotFound() => Status = EnvelopePageStatus.NotFound;
// ── Event ──
public event Action? OnChange;
private void NotifyStateChanged() => OnChange?.Invoke();
}
/// <summary>Alle möglichen Zustände der Umschlag-Seite</summary>
public enum EnvelopePageStatus
{
Loading,
RequiresAccessCode,
RequiresTwoFactor,
ShowDocument,
AlreadySigned,
Rejected,
NotFound,
Expired,
Error
}

View File

@@ -1,17 +0,0 @@
@using System.Net.Http
@using System.Net.Http.Json
@using Microsoft.AspNetCore.Components.Authorization
@using Microsoft.AspNetCore.Components.Forms
@using Microsoft.AspNetCore.Components.Routing
@using Microsoft.AspNetCore.Components.Web
@using static Microsoft.AspNetCore.Components.Web.RenderMode
@using Microsoft.AspNetCore.Components.Web.Virtualization
@using Microsoft.JSInterop
@using EnvelopeGenerator.ReceiverUI.Client
@using EnvelopeGenerator.ReceiverUI.Client.Models
@using EnvelopeGenerator.ReceiverUI.Client.Services
@using EnvelopeGenerator.ReceiverUI.Client.Services.Base
@using EnvelopeGenerator.ReceiverUI.Client.State
@using EnvelopeGenerator.ReceiverUI.Client.Auth
@using EnvelopeGenerator.ReceiverUI.Client.Components.Shared
@using EnvelopeGenerator.ReceiverUI.Client.Components.Envelope

View File

@@ -1,21 +0,0 @@
<!DOCTYPE html>
<html lang="de">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<base href="/" />
<link rel="stylesheet" href="bootstrap/bootstrap.min.css" />
<link rel="stylesheet" href="bootstrap-icons/bootstrap-icons.min.css" />
<link rel="stylesheet" href="app.css" />
<link rel="stylesheet" href="EnvelopeGenerator.ReceiverUI.styles.css" />
<link rel="icon" type="image/png" href="favicon.png" />
<HeadOutlet />
</head>
<body>
<Routes />
<script src="_framework/blazor.web.js"></script>
</body>
</html>

View File

@@ -1,87 +0,0 @@
@* MainLayout: Das Grundgerüst jeder Seite.
Entspricht _Layout.cshtml im Web-Projekt.
Aufbau:
- Header: signFLOW-Logo/Titel (oben, sticky)
- Main: Der Seiteninhalt (@Body) mit ErrorBoundary
- Footer: Copyright + Privacy-Link (unten)
Sticky Footer Pattern: Der Footer klebt immer am unteren Rand,
auch wenn der Inhalt wenig Platz braucht. Das funktioniert über
Flexbox in app.css (.app-container mit min-height: 100vh). *@
@inherits LayoutComponentBase
<div class="app-container">
<Toast />
@* ── Header ── *@
<header class="app-header">
<div class="d-flex align-items-center gap-2">
@* Im Web-Projekt steht hier ein <img> mit dem signFLOW-Logo.
Wir nutzen erstmal Text. Das Logo kommt in Phase 6
wenn wir die Bilder aus dem Web-Projekt portieren. *@
<span class="app-title">signFLOW</span>
</div>
</header>
@* ── Main: Seiteninhalt mit Error-Schutz ──
ErrorBoundary fängt unbehandelte Exceptions in Komponenten ab.
Ohne ErrorBoundary würde die gesamte App abstürzen.
Mit ErrorBoundary zeigen wir stattdessen eine Fehlermeldung
und einen "Erneut versuchen"-Button. *@
<main class="app-main">
<ErrorBoundary @ref="_errorBoundary">
<ChildContent>
@Body
</ChildContent>
<ErrorContent Context="ex">
<div class="error-container text-center py-5">
<div class="status-icon locked mb-3">
<i class="bi bi-exclamation-triangle"></i>
</div>
<h2>Ein unerwarteter Fehler ist aufgetreten</h2>
<p class="text-muted">Bitte versuchen Sie es erneut.</p>
<button class="btn btn-primary" @onclick="Recover">
<i class="bi bi-arrow-counterclockwise me-2"></i>
Erneut versuchen
</button>
</div>
</ErrorContent>
</ErrorBoundary>
</main>
@* ── Footer ──
Im Web-Projekt gibt es hier drei Elemente:
1. Copyright + Link zur Firmenwebsite
2. Sprachauswahl (Dropdown mit Flaggen) → kommt in Phase 6
3. Privacy-Link (Datenschutzerklärung)
Die Datenschutz-HTML-Dateien existieren im Web-Projekt unter
wwwroot/privacy-policy.de-DE.html. Wir verlinken vorerst
auf eine statische URL. Die Datei selbst portieren wir in Phase 6. *@
<footer class="app-footer">
<small>
&copy; signFLOW @DateTime.Now.Year
<a href="https://digitaldata.works" target="_blank" class="text-muted text-decoration-none">
Digital Data GmbH
</a>
</small>
@* Platzhalter für Sprachauswahl — kommt in Phase 6 *@
<a href="/privacy-policy.de-DE.html" target="_blank" class="text-muted text-decoration-none">
<small>Datenschutz</small>
</a>
</footer>
</div>
@code {
private ErrorBoundary? _errorBoundary;
/// <summary>
/// Setzt die ErrorBoundary zurück.
/// Blazor rendert dann @Body erneut statt der Fehlermeldung.
/// </summary>
private void Recover() => _errorBoundary?.Recover();
}

Some files were not shown because too many files have changed in this diff Show More