diff --git a/COPILOT_CONTEXT.md b/COPILOT_CONTEXT.md index 995ba9c8..ddb49ee7 100644 --- a/COPILOT_CONTEXT.md +++ b/COPILOT_CONTEXT.md @@ -1,4 +1,4 @@ -# EnvelopeGenerator — AI Context Reference +# EnvelopeGenerator — AI Context Reference ## Purpose Digital document signing system with **unified Blazor Auto (Server+WASM hybrid) frontend** for both Senders and Receivers. Senders create envelopes and place signature fields. Receivers view PDFs, sign documents, export stamped PDFs. @@ -125,6 +125,71 @@ Client ? WebUI:XXXX (Blazor Auto) --- +## Localization & Culture Management + +**Current Architecture:** Blazor WebAssembly (client-side culture management) + +### Implementation Details + +**Culture Storage:** +- Culture preference stored in browser's `localStorage` (key: `AppCulture`) +- Managed by `CultureService.cs` (ReceiverUI/Services) +- Supported cultures: `de-DE`, `en-US`, `fr-FR` + +**Culture Initialization:** +- **Location:** `Program.cs` (lines 53-57) +- Sets `CultureInfo.DefaultThreadCurrentCulture/UICulture` **before** app runs +- **WASM-Safe:** Each user has isolated browser instance + +**Language Selector:** +- **Component:** `LanguageSelector.razor` (ReceiverUI/Shared) +- Displays flag icon + language name +- Changes culture via `CultureService.SetCultureAsync()` +- Navigates with `forceLoad: false` (smooth transition, no page reload) + +### ⚠️ MIGRATION WARNING: Blazor Server/Auto + +**Current approach is WASM-specific and will break in Server/Auto render modes!** + +**Why it breaks:** +- `Program.cs:53-57` sets **global** `DefaultThreadCurrentCulture` +- In Server/Auto, one app instance serves **all users** +- User A selects German → User B sees German too (shared state) +- Thread-safety issues and culture conflicts + +**Migration Checklist (when moving to Server/Auto):** + +1. **Remove global culture initialization** from `Program.cs` (lines 53-57) + - See detailed warning comment in the code + +2. **Add RequestLocalizationMiddleware** (Server-side approach): + ```csharp + app.UseRequestLocalization(options => { + options.SupportedCultures = new[] { "de-DE", "en-US", "fr-FR" }; + options.SupportedUICultures = options.SupportedCultures; + options.RequestCultureProviders.Insert(0, new CookieRequestCultureProvider()); + }); + ``` + +3. **OR** Use **per-circuit culture** (Blazor Server approach): + - Store culture in circuit-scoped service + - Use `CascadingParameter` to distribute to components + - See: https://learn.microsoft.com/aspnet/core/blazor/globalization-localization + +4. **Update `LanguageSelector.razor`:** + - Remove manual `CultureInfo.DefaultThreadCurrentCulture` assignment + - Use middleware/circuit culture provider instead + +5. **Update `CultureService.cs`:** + - Integrate with Server-side culture provider + - May need to store in cookies instead of localStorage + +**References:** +- Microsoft Docs: [Blazor Globalization/Localization](https://learn.microsoft.com/aspnet/core/blazor/globalization-localization) +- Current implementation: `Program.cs`, `CultureService.cs`, `LanguageSelector.razor` + +--- + ## Key Files & Routes ### Client-Side Pages (WebUI.Client) @@ -156,7 +221,7 @@ Client ? WebUI:XXXX (Blazor Auto) --- -## Coordinate System — CRITICAL +## Coordinate System — CRITICAL **Database Format:** INCHES (GdPicture14 native) **Origin:** Top-left corner @@ -185,7 +250,7 @@ Client ? WebUI:XXXX (Blazor Auto) --- -## EnvelopeReceiver — PDF.js Viewer & Signing +## EnvelopeReceiver — PDF.js Viewer & Signing **Route:** `/envelope/{EnvelopeKey}` **Tech:** PDF.js 3.11.174 + Blazor Server (`@rendermode InteractiveServer`) + configurable quality @@ -228,7 +293,7 @@ window.pdfViewer = { --- -## Signature Workflow — EnvelopeReceiver +## Signature Workflow — EnvelopeReceiver **IMPORTANT:** iText7 NOT used (GPL license issue). Client-side overlay system only. @@ -281,9 +346,9 @@ public sealed record SignatureCaptureDto { ### API Endpoints **Controller:** `API/Controllers/CacheController.cs` -- `POST /api/Cache/SignatureCapture/{envelopeKey}` — Save -- `GET /api/Cache/SignatureCapture/{envelopeKey}` — Load -- `DELETE /api/Cache/SignatureCapture/{envelopeKey}` — Delete +- `POST /api/Cache/SignatureCapture/{envelopeKey}` — Save +- `GET /api/Cache/SignatureCapture/{envelopeKey}` — Load +- `DELETE /api/Cache/SignatureCapture/{envelopeKey}` — Delete **Cache Key Format:** ``` @@ -349,7 +414,7 @@ public async Task LoginSenderAsync(string username, string pa **Response:** - `200 OK` ? Cookie set, redirect to `/sender` -- `401 Unauthorized` ? Show error: "Ungültige Anmeldedaten" +- `401 Unauthorized` ? Show error: "UngĂĽltige Anmeldedaten" - Other ? Show error: "Serverfehler" **Cookie:** HTTP-only, Secure (HTTPS), SameSite=Strict @@ -407,7 +472,7 @@ public async Task LoginEnvelopeReceiverAsync(string key, st --- -## Mistakes History — Do NOT Repeat +## Mistakes History — Do NOT Repeat | Mistake | Why Wrong | |---|---| @@ -426,9 +491,9 @@ public async Task LoginEnvelopeReceiverAsync(string key, st ### Deprecated Projects **DO NOT USE:** -- `EnvelopeGenerator.ReceiverUI` (Pure Blazor WASM) — Migrated to WebUI (DevExpress compatibility issue) -- `EnvelopeGenerator.Web` (Razor Pages) — Replaced by unified WebUI -- PSPDFKit — Removed, use PDF.js + DevExpress instead +- `EnvelopeGenerator.ReceiverUI` (Pure Blazor WASM) — Migrated to WebUI (DevExpress compatibility issue) +- `EnvelopeGenerator.Web` (Razor Pages) — Replaced by unified WebUI +- PSPDFKit — Removed, use PDF.js + DevExpress instead ### Legacy Projects (VB.NET) **DO NOT TOUCH:** `EnvelopeGenerator.Service`, `EnvelopeGenerator.Form`, `EnvelopeGenerator.BBTests` @@ -453,8 +518,8 @@ Proves database uses INCHES natively. ## Quick Reference ### When working with coordinates: -1. **Database ? UI:** INCHES × 72 = PDF Points -2. **UI ? Display:** Points × scale = Pixels +1. **Database ? UI:** INCHES Ă— 72 = PDF Points +2. **UI ? Display:** Points Ă— scale = Pixels 3. **iText7 stamping:** Flip Y-axis (top-down ? bottom-up) ### When adding features: diff --git a/EnvelopeGenerator.API/Controllers/AuthController.cs b/EnvelopeGenerator.API/Controllers/AuthController.cs index 6a3500c9..56e710d8 100644 --- a/EnvelopeGenerator.API/Controllers/AuthController.cs +++ b/EnvelopeGenerator.API/Controllers/AuthController.cs @@ -40,7 +40,7 @@ public partial class AuthController(IOptions authTokenKeyOptions, /// Wenn es kein zugelassenes Cookie gibt, wird „nicht zugelassen“ zurĂĽckgegeben. [ProducesResponseType(typeof(void), StatusCodes.Status200OK)] [ProducesResponseType(typeof(void), StatusCodes.Status401Unauthorized)] - [Authorize(Policy = AuthPolicy.SenderOrReceiver)] + [Authorize(AuthenticationSchemes = AuthScheme.Sender)] [HttpPost("logout")] public async Task Logout() { @@ -69,7 +69,7 @@ public partial class AuthController(IOptions authTokenKeyOptions, [ProducesResponseType(typeof(void), StatusCodes.Status200OK)] [ProducesResponseType(typeof(void), StatusCodes.Status401Unauthorized)] [HttpGet("check")] - [Authorize] + [Authorize(AuthenticationSchemes = AuthScheme.Sender)] public IActionResult Check(string? role = null) => role is not null && !User.IsInRole(role) ? Unauthorized() diff --git a/EnvelopeGenerator.API/Controllers/ConfigController.cs b/EnvelopeGenerator.API/Controllers/ConfigController.cs index 81aa23c0..117252a2 100644 --- a/EnvelopeGenerator.API/Controllers/ConfigController.cs +++ b/EnvelopeGenerator.API/Controllers/ConfigController.cs @@ -1,4 +1,5 @@ using EnvelopeGenerator.API.Models.PsPdfKitAnnotation; +using EnvelopeGenerator.Domain.Constants; using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Mvc; using Microsoft.Extensions.Options; @@ -13,7 +14,7 @@ namespace EnvelopeGenerator.API.Controllers; /// [Route("api/[controller]")] [ApiController] -[Authorize] +[Authorize(Policy = AuthPolicy.SenderOrReceiver)] public class ConfigController(IOptionsMonitor annotationParamsOptions) : ControllerBase { private readonly AnnotationParams _annotationParams = annotationParamsOptions.CurrentValue; diff --git a/EnvelopeGenerator.API/Controllers/SignatureController.cs b/EnvelopeGenerator.API/Controllers/DocReceiverElementController.cs similarity index 88% rename from EnvelopeGenerator.API/Controllers/SignatureController.cs rename to EnvelopeGenerator.API/Controllers/DocReceiverElementController.cs index bd3ff352..6c234117 100644 --- a/EnvelopeGenerator.API/Controllers/SignatureController.cs +++ b/EnvelopeGenerator.API/Controllers/DocReceiverElementController.cs @@ -15,14 +15,14 @@ namespace EnvelopeGenerator.API.Controllers; [Authorize(Policy = AuthPolicy.Receiver)] [ApiController] [Route("api/[controller]")] -public class SignatureController : ControllerBase +public class DocReceiverElementController : ControllerBase { private readonly IMediator _mediator; /// - /// Initializes a new instance of . + /// Initializes a new instance of . /// - public SignatureController(IMediator mediator) + public DocReceiverElementController(IMediator mediator) { _mediator = mediator; } diff --git a/EnvelopeGenerator.API/EnvelopeGenerator.API.csproj b/EnvelopeGenerator.API/EnvelopeGenerator.API.csproj index ba382a62..96154a76 100644 --- a/EnvelopeGenerator.API/EnvelopeGenerator.API.csproj +++ b/EnvelopeGenerator.API/EnvelopeGenerator.API.csproj @@ -34,6 +34,7 @@ + diff --git a/EnvelopeGenerator.API/Program.cs b/EnvelopeGenerator.API/Program.cs index 2ea3ce27..8e0b570e 100644 --- a/EnvelopeGenerator.API/Program.cs +++ b/EnvelopeGenerator.API/Program.cs @@ -13,7 +13,6 @@ using EnvelopeGenerator.Application; using DigitalData.Auth.Client; using DigitalData.Core.Abstractions; using EnvelopeGenerator.API.Models; -using Microsoft.AspNetCore.Authentication.JwtBearer; using Microsoft.IdentityModel.Tokens; using DigitalData.Core.Abstractions.Security.Extensions; using EnvelopeGenerator.API.Middleware; @@ -22,6 +21,7 @@ using NLog.Web; using NLog; using DigitalData.Auth.Claims; using EnvelopeGenerator.API; +using Microsoft.AspNetCore.Authentication.JwtBearer; var logger = LogManager.Setup().LoadConfigurationFromAppSettings().GetCurrentClassLogger(); logger.Info("Logging initialized!"); @@ -44,7 +44,11 @@ try var deferredProvider = new DeferredServiceProvider(); - builder.Services.AddControllers(); + builder.Services.AddControllers() + .AddJsonOptions(options => + { + options.JsonSerializerOptions.ReferenceHandler = System.Text.Json.Serialization.ReferenceHandler.IgnoreCycles; + }); builder.Services.AddHttpClient(); builder.Services.AddReverseProxy().LoadFromConfig(builder.Configuration.GetSection("ReverseProxy")); @@ -238,8 +242,9 @@ try }); builder.Services.AddAuthorizationBuilder() - .AddPolicy(AuthPolicy.SenderOrReceiver, policy => policy.RequireRole(Role.Sender, Role.Receiver.Full)) - + .AddPolicy(AuthPolicy.SenderOrReceiver, policy => policy + .RequireRole(Role.Sender, Role.Receiver.Full) + .AddAuthenticationSchemes(AuthScheme.Sender, AuthScheme.Receiver)) .AddPolicy(AuthPolicy.Sender, policy => policy .RequireRole(Role.Sender) .AddAuthenticationSchemes(AuthScheme.Sender)) diff --git a/EnvelopeGenerator.API/Properties/launchSettings.json b/EnvelopeGenerator.API/Properties/launchSettings.json index 1b89a1c9..365fa0b2 100644 --- a/EnvelopeGenerator.API/Properties/launchSettings.json +++ b/EnvelopeGenerator.API/Properties/launchSettings.json @@ -22,8 +22,8 @@ "https": { "commandName": "Project", "dotnetRunMessages": true, - "launchBrowser": false, - "launchUrl": "swagger", + "launchBrowser": true, + "launchUrl": "sender", "applicationUrl": "https://localhost:8088;http://localhost:5131", "environmentVariables": { "ASPNETCORE_ENVIRONMENT": "Development" diff --git a/EnvelopeGenerator.Application/Common/Dto/ConfigDto.cs b/EnvelopeGenerator.Application/Common/Dto/ConfigDto.cs index 8b9505a4..5a05e3fa 100644 --- a/EnvelopeGenerator.Application/Common/Dto/ConfigDto.cs +++ b/EnvelopeGenerator.Application/Common/Dto/ConfigDto.cs @@ -1,11 +1,8 @@ -using Microsoft.AspNetCore.Mvc; - -namespace EnvelopeGenerator.Application.Common.Dto; +namespace EnvelopeGenerator.Application.Common.Dto; /// /// Data Transfer Object representing configuration settings. /// -[ApiExplorerSettings(IgnoreApi = true)] public class ConfigDto { /// diff --git a/EnvelopeGenerator.Application/Common/Dto/DocReceiverElementDto.cs b/EnvelopeGenerator.Application/Common/Dto/DocReceiverElementDto.cs index d24cee32..808561f6 100644 --- a/EnvelopeGenerator.Application/Common/Dto/DocReceiverElementDto.cs +++ b/EnvelopeGenerator.Application/Common/Dto/DocReceiverElementDto.cs @@ -1,14 +1,11 @@ using EnvelopeGenerator.Domain.Constants; using EnvelopeGenerator.Domain.Interfaces; -using Microsoft.AspNetCore.Mvc; -using System.ComponentModel.DataAnnotations.Schema; namespace EnvelopeGenerator.Application.Common.Dto; /// /// Data Transfer Object representing a positioned element assigned to a document receiver. /// -[ApiExplorerSettings(IgnoreApi = true)] public class DocReceiverElementDto : IDocReceiverElement { /// diff --git a/EnvelopeGenerator.Application/Common/Dto/DocumentDto.cs b/EnvelopeGenerator.Application/Common/Dto/DocumentDto.cs index 4fa3394e..34403d8f 100644 --- a/EnvelopeGenerator.Application/Common/Dto/DocumentDto.cs +++ b/EnvelopeGenerator.Application/Common/Dto/DocumentDto.cs @@ -1,11 +1,8 @@ -using Microsoft.AspNetCore.Mvc; - -namespace EnvelopeGenerator.Application.Common.Dto; +namespace EnvelopeGenerator.Application.Common.Dto; /// /// Data Transfer Object representing a document within an envelope, including optional binary data and form elements. /// -[ApiExplorerSettings(IgnoreApi = true)] public class DocumentDto { /// diff --git a/EnvelopeGenerator.Application/Common/Dto/DocumentStatusDto.cs b/EnvelopeGenerator.Application/Common/Dto/DocumentStatusDto.cs index dd37172f..0bb8a35e 100644 --- a/EnvelopeGenerator.Application/Common/Dto/DocumentStatusDto.cs +++ b/EnvelopeGenerator.Application/Common/Dto/DocumentStatusDto.cs @@ -1,12 +1,10 @@ using EnvelopeGenerator.Domain.Constants; -using Microsoft.AspNetCore.Mvc; namespace EnvelopeGenerator.Application.Common.Dto; /// /// Data Transfer Object representing the status of a document for a specific receiver. /// -[ApiExplorerSettings(IgnoreApi = true)] public class DocumentStatusDto { /// diff --git a/EnvelopeGenerator.Application/Common/Dto/EnvelopeDto.cs b/EnvelopeGenerator.Application/Common/Dto/EnvelopeDto.cs index 739ce86f..601ac1b0 100644 --- a/EnvelopeGenerator.Application/Common/Dto/EnvelopeDto.cs +++ b/EnvelopeGenerator.Application/Common/Dto/EnvelopeDto.cs @@ -1,19 +1,14 @@ using DigitalData.EmailProfilerDispatcher.Abstraction.Attributes; using DigitalData.UserManager.Application.DTOs.User; using EnvelopeGenerator.Application.Common.Dto.EnvelopeReceiver; -using EnvelopeGenerator.Application.Common.Dto.Receiver; using EnvelopeGenerator.Domain.Constants; using EnvelopeGenerator.Domain.Entities; using EnvelopeGenerator.Domain.Interfaces; -using Microsoft.AspNetCore.Mvc; -using System.Text.Json.Serialization; - namespace EnvelopeGenerator.Application.Common.Dto; /// /// /// -[ApiExplorerSettings(IgnoreApi = true)] public record EnvelopeDto : IEnvelope { /// @@ -133,5 +128,5 @@ public record EnvelopeDto : IEnvelope /// /// /// - public IEnumerable? Receivers { get; set; } + public IEnumerable? EnvelopeReceivers { get; set; } } \ No newline at end of file diff --git a/EnvelopeGenerator.Application/Common/Dto/EnvelopeReceiver/EnvelopeReceiverDto.cs b/EnvelopeGenerator.Application/Common/Dto/EnvelopeReceiver/EnvelopeReceiverDto.cs index 3ec59e15..34a74c2b 100644 --- a/EnvelopeGenerator.Application/Common/Dto/EnvelopeReceiver/EnvelopeReceiverDto.cs +++ b/EnvelopeGenerator.Application/Common/Dto/EnvelopeReceiver/EnvelopeReceiverDto.cs @@ -1,13 +1,11 @@ using DigitalData.EmailProfilerDispatcher.Abstraction.Attributes; using EnvelopeGenerator.Application.Common.Dto.Receiver; -using Microsoft.AspNetCore.Mvc; namespace EnvelopeGenerator.Application.Common.Dto.EnvelopeReceiver; /// /// /// -[ApiExplorerSettings(IgnoreApi = true)] public record EnvelopeReceiverDto { /// diff --git a/EnvelopeGenerator.Application/Common/Dto/EnvelopeReceiver/EnvelopeReceiverSecretDto.cs b/EnvelopeGenerator.Application/Common/Dto/EnvelopeReceiver/EnvelopeReceiverSecretDto.cs index 717a358f..ad2c0f20 100644 --- a/EnvelopeGenerator.Application/Common/Dto/EnvelopeReceiver/EnvelopeReceiverSecretDto.cs +++ b/EnvelopeGenerator.Application/Common/Dto/EnvelopeReceiver/EnvelopeReceiverSecretDto.cs @@ -1,11 +1,8 @@ -using Microsoft.AspNetCore.Mvc; - -namespace EnvelopeGenerator.Application.Common.Dto.EnvelopeReceiver; +namespace EnvelopeGenerator.Application.Common.Dto.EnvelopeReceiver; /// /// /// -[ApiExplorerSettings(IgnoreApi = true)] public record EnvelopeReceiverSecretDto : EnvelopeReceiverDto { /// diff --git a/EnvelopeGenerator.Application/Common/Dto/EnvelopeReceiverReadOnly/EnvelopeReceiverReadOnlyCreateDto.cs b/EnvelopeGenerator.Application/Common/Dto/EnvelopeReceiverReadOnly/EnvelopeReceiverReadOnlyCreateDto.cs index 506f7f6b..212aa3f1 100644 --- a/EnvelopeGenerator.Application/Common/Dto/EnvelopeReceiverReadOnly/EnvelopeReceiverReadOnlyCreateDto.cs +++ b/EnvelopeGenerator.Application/Common/Dto/EnvelopeReceiverReadOnly/EnvelopeReceiverReadOnlyCreateDto.cs @@ -1,5 +1,4 @@ -using Microsoft.AspNetCore.Mvc; -using System.ComponentModel.DataAnnotations; +using System.ComponentModel.DataAnnotations; using System.Text.Json.Serialization; namespace EnvelopeGenerator.Application.Common.Dto.EnvelopeReceiverReadOnly; @@ -8,7 +7,6 @@ namespace EnvelopeGenerator.Application.Common.Dto.EnvelopeReceiverReadOnly; /// /// /// -[ApiExplorerSettings(IgnoreApi = true)] public record EnvelopeReceiverReadOnlyCreateDto( DateTime DateValid) { diff --git a/EnvelopeGenerator.Application/Common/Dto/EnvelopeReceiverReadOnly/EnvelopeReceiverReadOnlyDto.cs b/EnvelopeGenerator.Application/Common/Dto/EnvelopeReceiverReadOnly/EnvelopeReceiverReadOnlyDto.cs index 89ff3716..5a9de959 100644 --- a/EnvelopeGenerator.Application/Common/Dto/EnvelopeReceiverReadOnly/EnvelopeReceiverReadOnlyDto.cs +++ b/EnvelopeGenerator.Application/Common/Dto/EnvelopeReceiverReadOnly/EnvelopeReceiverReadOnlyDto.cs @@ -1,6 +1,4 @@ -using EnvelopeGenerator.Application.Common.Dto; -using EnvelopeGenerator.Application.Common.Dto.Receiver; -using Microsoft.AspNetCore.Mvc; +using EnvelopeGenerator.Application.Common.Dto.Receiver; namespace EnvelopeGenerator.Application.Common.Dto.EnvelopeReceiverReadOnly; @@ -8,7 +6,6 @@ namespace EnvelopeGenerator.Application.Common.Dto.EnvelopeReceiverReadOnly; /// Represents a read-only Data Transfer Object (DTO) for an envelope receiver. /// Contains information about the receiver, associated envelope, and audit details. /// -[ApiExplorerSettings(IgnoreApi = true)] public class EnvelopeReceiverReadOnlyDto { /// diff --git a/EnvelopeGenerator.Application/Common/Dto/EnvelopeReceiverReadOnly/EnvelopeReceiverReadOnlyUpdateDto.cs b/EnvelopeGenerator.Application/Common/Dto/EnvelopeReceiverReadOnly/EnvelopeReceiverReadOnlyUpdateDto.cs index 2e2a384a..3db17e10 100644 --- a/EnvelopeGenerator.Application/Common/Dto/EnvelopeReceiverReadOnly/EnvelopeReceiverReadOnlyUpdateDto.cs +++ b/EnvelopeGenerator.Application/Common/Dto/EnvelopeReceiverReadOnly/EnvelopeReceiverReadOnlyUpdateDto.cs @@ -1,11 +1,8 @@ -using Microsoft.AspNetCore.Mvc; - -namespace EnvelopeGenerator.Application.Common.Dto.EnvelopeReceiverReadOnly; +namespace EnvelopeGenerator.Application.Common.Dto.EnvelopeReceiverReadOnly; /// /// Data Transfer Object for updating a read-only envelope receiver. /// -[ApiExplorerSettings(IgnoreApi = true)] public class EnvelopeReceiverReadOnlyUpdateDto { /// diff --git a/EnvelopeGenerator.Application/Common/Dto/EnvelopeTypeDto.cs b/EnvelopeGenerator.Application/Common/Dto/EnvelopeTypeDto.cs index ccbdd9db..cb2e4578 100644 --- a/EnvelopeGenerator.Application/Common/Dto/EnvelopeTypeDto.cs +++ b/EnvelopeGenerator.Application/Common/Dto/EnvelopeTypeDto.cs @@ -1,11 +1,8 @@ -using Microsoft.AspNetCore.Mvc; - -namespace EnvelopeGenerator.Application.Common.Dto; +namespace EnvelopeGenerator.Application.Common.Dto; /// /// Data Transfer Object representing a type of envelope with its configuration settings. /// -[ApiExplorerSettings(IgnoreApi = true)] public class EnvelopeTypeDto { /// diff --git a/EnvelopeGenerator.Application/Common/Dto/Messaging/GtxMessagingResponse.cs b/EnvelopeGenerator.Application/Common/Dto/Messaging/GtxMessagingResponse.cs index 614da32c..5d8b008c 100644 --- a/EnvelopeGenerator.Application/Common/Dto/Messaging/GtxMessagingResponse.cs +++ b/EnvelopeGenerator.Application/Common/Dto/Messaging/GtxMessagingResponse.cs @@ -1,9 +1,6 @@ -using Microsoft.AspNetCore.Mvc; - -namespace EnvelopeGenerator.Application.Common.Dto.Messaging; +namespace EnvelopeGenerator.Application.Common.Dto.Messaging; /// /// /// -[ApiExplorerSettings(IgnoreApi = true)] public class GtxMessagingResponse : Dictionary { } \ No newline at end of file diff --git a/EnvelopeGenerator.Application/Common/Dto/Messaging/SmsResponse.cs b/EnvelopeGenerator.Application/Common/Dto/Messaging/SmsResponse.cs index f1f71123..36c6f774 100644 --- a/EnvelopeGenerator.Application/Common/Dto/Messaging/SmsResponse.cs +++ b/EnvelopeGenerator.Application/Common/Dto/Messaging/SmsResponse.cs @@ -1,11 +1,8 @@ -using Microsoft.AspNetCore.Mvc; - -namespace EnvelopeGenerator.Application.Common.Dto.Messaging; +namespace EnvelopeGenerator.Application.Common.Dto.Messaging; /// /// /// -[ApiExplorerSettings(IgnoreApi = true)] public record SmsResponse { /// diff --git a/EnvelopeGenerator.Application/Common/Dto/Receiver/ReceiverDto.cs b/EnvelopeGenerator.Application/Common/Dto/Receiver/ReceiverDto.cs index 534a33f5..956868ce 100644 --- a/EnvelopeGenerator.Application/Common/Dto/Receiver/ReceiverDto.cs +++ b/EnvelopeGenerator.Application/Common/Dto/Receiver/ReceiverDto.cs @@ -1,5 +1,4 @@ using EnvelopeGenerator.Application.Common.Dto.EnvelopeReceiver; -using Microsoft.AspNetCore.Mvc; using System.Text.Json.Serialization; namespace EnvelopeGenerator.Application.Common.Dto.Receiver; @@ -7,7 +6,6 @@ namespace EnvelopeGenerator.Application.Common.Dto.Receiver; /// /// /// -[ApiExplorerSettings(IgnoreApi = true)] public class ReceiverDto { /// diff --git a/EnvelopeGenerator.Application/Common/Dto/MappingProfile.cs b/EnvelopeGenerator.Application/Common/MappingProfile.cs similarity index 60% rename from EnvelopeGenerator.Application/Common/Dto/MappingProfile.cs rename to EnvelopeGenerator.Application/Common/MappingProfile.cs index d46be867..361cd0cd 100644 --- a/EnvelopeGenerator.Application/Common/Dto/MappingProfile.cs +++ b/EnvelopeGenerator.Application/Common/MappingProfile.cs @@ -7,7 +7,9 @@ using EnvelopeGenerator.Application.Common.Dto.Receiver; using EnvelopeGenerator.Application.Common.Extensions; using EnvelopeGenerator.Domain.Entities; -namespace EnvelopeGenerator.Application.Common.Dto; +using EnvelopeGenerator.Application.Common.Dto; + +namespace EnvelopeGenerator.Application.Common; /// /// Represents the AutoMapper profile configuration for mapping between @@ -26,15 +28,15 @@ public class MappingProfile : Profile CreateMap(); CreateMap(); CreateMap(); - CreateMap().ForMember(dest => dest.Receivers, opt => opt.MapFrom(src => src.EnvelopeReceivers.Select(er => er.Receiver))); + CreateMap(); CreateMap(); - CreateMap().ForMember(dest => dest.ActionDate, opt => opt.MapFrom(src => src.ChangedWhen)); - CreateMap().ForMember(dest => dest.ActionDate, opt => opt.MapFrom(src => src.ChangedWhen)); - CreateMap(); - CreateMap(); + CreateMap().ForMember(dest => dest.ActionDate, opt => opt.MapFrom(src => src.ChangedWhen)); + CreateMap().ForMember(dest => dest.ActionDate, opt => opt.MapFrom(src => src.ChangedWhen)); + CreateMap(); + CreateMap(); CreateMap(); - CreateMap(); - CreateMap(); + CreateMap(); + CreateMap(); CreateMap(); // DTO to Entity mappings @@ -47,13 +49,13 @@ public class MappingProfile : Profile CreateMap(); CreateMap(); CreateMap(); - CreateMap().ForMember(dest => dest.ChangedWhen, opt => opt.MapFrom(src => src.ActionDate)); - CreateMap().ForMember(dest => dest.ChangedWhen, opt => opt.MapFrom(src => src.ActionDate)); - CreateMap(); + CreateMap().ForMember(dest => dest.ChangedWhen, opt => opt.MapFrom(src => src.ActionDate)); + CreateMap().ForMember(dest => dest.ChangedWhen, opt => opt.MapFrom(src => src.ActionDate)); + CreateMap(); CreateMap(); - CreateMap().ForMember(rcv => rcv.EnvelopeReceivers, rcvReadDto => rcvReadDto.Ignore()); - CreateMap(); - CreateMap(); + CreateMap().ForMember(rcv => rcv.EnvelopeReceivers, rcvReadDto => rcvReadDto.Ignore()); + CreateMap(); + CreateMap(); CreateMap() .MapAddedWhen(); diff --git a/EnvelopeGenerator.Application/EnvelopeGenerator.Application.csproj b/EnvelopeGenerator.Application/EnvelopeGenerator.Application.csproj index ba181291..5ea118b7 100644 --- a/EnvelopeGenerator.Application/EnvelopeGenerator.Application.csproj +++ b/EnvelopeGenerator.Application/EnvelopeGenerator.Application.csproj @@ -7,7 +7,7 @@ true bin\$(Configuration)\$(TargetFramework)\$(AssemblyName).xml - + @@ -21,7 +21,6 @@ - @@ -79,25 +78,25 @@ - - - + + + 7.0.5 - + - - - 8.1.1 - + + + 8.1.1 + - - - 8.1.1 - + + + 8.1.1 + diff --git a/EnvelopeGenerator.Application/Envelopes/Queries/ReadEnvelopeQuery.cs b/EnvelopeGenerator.Application/Envelopes/Queries/ReadEnvelopeQuery.cs index 0719279e..8af36db6 100644 --- a/EnvelopeGenerator.Application/Envelopes/Queries/ReadEnvelopeQuery.cs +++ b/EnvelopeGenerator.Application/Envelopes/Queries/ReadEnvelopeQuery.cs @@ -14,6 +14,16 @@ namespace EnvelopeGenerator.Application.Envelopes.Queries; /// public record ReadEnvelopeQuery : EnvelopeQueryBase, IRequest> { + /// + /// + /// + public bool OnlyActive { get; init; } = false; + + /// + /// + /// + public bool OnlyCompleted { get; init; } = false; + /// /// Abfrage des Include des Umschlags /// @@ -132,6 +142,12 @@ public class ReadEnvelopeQueryHandler : IRequestHandler !status.Ignore.Contains(e.Status)); } + if(request is { OnlyActive: true }) + query = query.Where(e => Status.Active.Contains(e.Status)); + + if (request is { OnlyCompleted: true }) + query = query.Where(e => Status.Completed.Contains(e.Status)); + var envelopes = await query .Include(e => e.EnvelopeReceivers).ThenInclude(er => er.Receiver) .ToListAsync(cancel); diff --git a/EnvelopeGenerator.Application/Receivers/Commands/CreateReceiverCommand.cs b/EnvelopeGenerator.Application/Receivers/Commands/CreateReceiverCommand.cs index 333f4a35..712e78ca 100644 --- a/EnvelopeGenerator.Application/Receivers/Commands/CreateReceiverCommand.cs +++ b/EnvelopeGenerator.Application/Receivers/Commands/CreateReceiverCommand.cs @@ -3,7 +3,6 @@ using DigitalData.Core.Abstraction.Application.Repository; using EnvelopeGenerator.Application.Common.Dto.Receiver; using EnvelopeGenerator.Domain.Entities; using MediatR; -using Microsoft.AspNetCore.Mvc; using Microsoft.EntityFrameworkCore; using System.ComponentModel.DataAnnotations; using System.Security.Cryptography; @@ -14,7 +13,6 @@ namespace EnvelopeGenerator.Application.Receivers.Commands; /// /// /// -[ApiExplorerSettings(IgnoreApi = true)] public record CreateReceiverCommand : IRequest<(ReceiverDto Receiver, bool AlreadyExists)> { /// diff --git a/EnvelopeGenerator.Application/Receivers/Commands/UpdateReceiverCommand.cs b/EnvelopeGenerator.Application/Receivers/Commands/UpdateReceiverCommand.cs index b9e211c2..9623d6d7 100644 --- a/EnvelopeGenerator.Application/Receivers/Commands/UpdateReceiverCommand.cs +++ b/EnvelopeGenerator.Application/Receivers/Commands/UpdateReceiverCommand.cs @@ -1,11 +1,8 @@ -using Microsoft.AspNetCore.Mvc; - -namespace EnvelopeGenerator.Application.Receivers.Commands; +namespace EnvelopeGenerator.Application.Receivers.Commands; /// /// Data Transfer Object for updating a receiver's information. /// -[ApiExplorerSettings(IgnoreApi = true)] public class UpdateReceiverCommand { /// diff --git a/EnvelopeGenerator.Application/Resources/Resource.cs b/EnvelopeGenerator.Application/Resources/Resource.cs index 13223106..630b45b1 100644 --- a/EnvelopeGenerator.Application/Resources/Resource.cs +++ b/EnvelopeGenerator.Application/Resources/Resource.cs @@ -397,4 +397,412 @@ public static class Extensions /// /// public static string LockedFooterBody(this IStringLocalizer localizer, string suffix) => localizer[nameof(LockedFooterBody) + suffix].Value; + + // Sender-side UI resources + + /// + /// + /// + /// + /// + public static string NewEnvelope(this IStringLocalizer localizer) => localizer[nameof(NewEnvelope)].Value; + + /// + /// + /// + /// + /// + public static string LoadEnvelope(this IStringLocalizer localizer) => localizer[nameof(LoadEnvelope)].Value; + + /// + /// + /// + /// + /// + public static string DeleteEnvelope(this IStringLocalizer localizer) => localizer[nameof(DeleteEnvelope)].Value; + + /// + /// + /// + /// + /// + public static string RefreshData(this IStringLocalizer localizer) => localizer[nameof(RefreshData)].Value; + + /// + /// + /// + /// + /// + public static string RefreshedAt(this IStringLocalizer localizer) => localizer[nameof(RefreshedAt)].Value; + + /// + /// + /// + /// + /// + public static string ShowDocument(this IStringLocalizer localizer) => localizer[nameof(ShowDocument)].Value; + + /// + /// + /// + /// + /// + public static string ContactReceiver(this IStringLocalizer localizer) => localizer[nameof(ContactReceiver)].Value; + + /// + /// + /// + /// + /// + public static string EnvelopeId(this IStringLocalizer localizer) => localizer[nameof(EnvelopeId)].Value; + + /// + /// + /// + /// + /// + public static string OpenLogDirectory(this IStringLocalizer localizer) => localizer[nameof(OpenLogDirectory)].Value; + + /// + /// + /// + /// + /// + public static string ShowResultsReport(this IStringLocalizer localizer) => localizer[nameof(ShowResultsReport)].Value; + + /// + /// + /// + /// + /// + public static string SupportMail(this IStringLocalizer localizer) => localizer[nameof(SupportMail)].Value; + + /// + /// + /// + /// + /// + public static string ResendInvitation(this IStringLocalizer localizer) => localizer[nameof(ResendInvitation)].Value; + + /// + /// + /// + /// + /// + public static string Export(this IStringLocalizer localizer) => localizer[nameof(Export)].Value; + + /// + /// + /// + /// + /// + public static string Receivers(this IStringLocalizer localizer) => localizer[nameof(Receivers)].Value; + + /// + /// + /// + /// + /// + public static string EmailSalutation(this IStringLocalizer localizer) => localizer[nameof(EmailSalutation)].Value; + + /// + /// + /// + /// + /// + public static string SignedWhen(this IStringLocalizer localizer) => localizer[nameof(SignedWhen)].Value; + + /// + /// + /// + /// + /// + public static string AccessCode(this IStringLocalizer localizer) => localizer[nameof(AccessCode)].Value; + + /// + /// + /// + /// + /// + public static string User(this IStringLocalizer localizer) => localizer[nameof(User)].Value; + + /// + /// + /// + /// + /// + public static string Type(this IStringLocalizer localizer) => localizer[nameof(Type)].Value; + + /// + /// + /// + /// + /// + public static string Title(this IStringLocalizer localizer) => localizer[nameof(Title)].Value; + + /// + /// + /// + /// + /// + public static string CreatedOn(this IStringLocalizer localizer) => localizer[nameof(CreatedOn)].Value; + + /// + /// + /// + /// + /// + public static string LastModified(this IStringLocalizer localizer) => localizer[nameof(LastModified)].Value; + + /// + /// + /// + /// + /// + public static string OpenEnvelopes(this IStringLocalizer localizer) => localizer[nameof(OpenEnvelopes)].Value; + + /// + /// + /// + /// + /// + public static string CompletedEnvelopes(this IStringLocalizer localizer) => localizer[nameof(CompletedEnvelopes)].Value; + + /// + /// + /// + /// + /// + public static string SendAccessCode(this IStringLocalizer localizer) => localizer[nameof(SendAccessCode)].Value; + + /// + /// + /// + /// + /// + public static string TwoFactorProperties(this IStringLocalizer localizer) => localizer[nameof(TwoFactorProperties)].Value; + + /// + /// + /// + /// + /// + public static string Name(this IStringLocalizer localizer) => localizer[nameof(Name)].Value; + + /// + /// + /// + /// + /// + public static string PhoneNumber(this IStringLocalizer localizer) => localizer[nameof(PhoneNumber)].Value; + + /// + /// + /// + /// + /// + public static string AddReceiver(this IStringLocalizer localizer) => localizer[nameof(AddReceiver)].Value; + + /// + /// + /// + /// + /// + public static string DeleteReceiver(this IStringLocalizer localizer) => localizer[nameof(DeleteReceiver)].Value; + + /// + /// + /// + /// + /// + public static string AddFile(this IStringLocalizer localizer) => localizer[nameof(AddFile)].Value; + + /// + /// + /// + /// + /// + public static string MergeFiles(this IStringLocalizer localizer) => localizer[nameof(MergeFiles)].Value; + + /// + /// + /// + /// + /// + public static string DeleteFile(this IStringLocalizer localizer) => localizer[nameof(DeleteFile)].Value; + + /// + /// + /// + /// + /// + public static string ShowFile(this IStringLocalizer localizer) => localizer[nameof(ShowFile)].Value; + + /// + /// + /// + /// + /// + public static string EditFields(this IStringLocalizer localizer) => localizer[nameof(EditFields)].Value; + + /// + /// + /// + /// + /// + public static string EditData(this IStringLocalizer localizer) => localizer[nameof(EditData)].Value; + + /// + /// + /// + /// + /// + public static string Save(this IStringLocalizer localizer) => localizer[nameof(Save)].Value; + + /// + /// + /// + /// + /// + public static string SendEnvelope(this IStringLocalizer localizer) => localizer[nameof(SendEnvelope)].Value; + + /// + /// + /// + /// + /// + public static string Cancel(this IStringLocalizer localizer) => localizer[nameof(Cancel)].Value; + + /// + /// + /// + /// + /// + public static string AddSignature(this IStringLocalizer localizer) => localizer[nameof(AddSignature)].Value; + + /// + /// + /// + /// + /// + public static string DeleteSignature(this IStringLocalizer localizer) => localizer[nameof(DeleteSignature)].Value; + + /// + /// + /// + /// + /// + public static string Language(this IStringLocalizer localizer) => localizer[nameof(Language)].Value; + + /// + /// + /// + /// + /// + public static string UseAccessCode(this IStringLocalizer localizer) => localizer[nameof(UseAccessCode)].Value; + + /// + /// + /// + /// + /// + public static string TwoFactorEnabled(this IStringLocalizer localizer) => localizer[nameof(TwoFactorEnabled)].Value; + + /// + /// + /// + /// + /// + public static string CertificationType(this IStringLocalizer localizer) => localizer[nameof(CertificationType)].Value; + + /// + /// + /// + /// + /// + public static string FinalEmailToCreator(this IStringLocalizer localizer) => localizer[nameof(FinalEmailToCreator)].Value; + + /// + /// + /// + /// + /// + public static string FinalEmailToReceivers(this IStringLocalizer localizer) => localizer[nameof(FinalEmailToReceivers)].Value; + + /// + /// + /// + /// + /// + public static string SendReminderEmails(this IStringLocalizer localizer) => localizer[nameof(SendReminderEmails)].Value; + + /// + /// + /// + /// + /// + public static string FirstReminderDays(this IStringLocalizer localizer) => localizer[nameof(FirstReminderDays)].Value; + + /// + /// + /// + /// + /// + public static string ReminderIntervalDays(this IStringLocalizer localizer) => localizer[nameof(ReminderIntervalDays)].Value; + + /// + /// + /// + /// + /// + public static string ExpiresWhenDays(this IStringLocalizer localizer) => localizer[nameof(ExpiresWhenDays)].Value; + + /// + /// + /// + /// + /// + public static string ExpiresWarningDays(this IStringLocalizer localizer) => localizer[nameof(ExpiresWarningDays)].Value; + + /// + /// + /// + /// + /// + public static string Message(this IStringLocalizer localizer) => localizer[nameof(Message)].Value; + + /// + /// + /// + /// + /// + public static string EnvelopeType(this IStringLocalizer localizer) => localizer[nameof(EnvelopeType)].Value; + + /// + /// + /// + /// + /// + public static string AllOptions(this IStringLocalizer localizer) => localizer[nameof(AllOptions)].Value; + + /// + /// + /// + /// + /// + public static string DeleteReason(this IStringLocalizer localizer) => localizer[nameof(DeleteReason)].Value; + + /// + /// + /// + /// + /// + public static string PleaseProvideReason(this IStringLocalizer localizer) => localizer[nameof(PleaseProvideReason)].Value; + + /// + /// + /// + /// + /// + public static string Status(this IStringLocalizer localizer) => localizer[nameof(Status)].Value; } \ No newline at end of file diff --git a/EnvelopeGenerator.Application/Resources/Resource.de-DE.resx b/EnvelopeGenerator.Application/Resources/Resource.de-DE.resx index 16fb00f5..223b8e72 100644 --- a/EnvelopeGenerator.Application/Resources/Resource.de-DE.resx +++ b/EnvelopeGenerator.Application/Resources/Resource.de-DE.resx @@ -477,4 +477,178 @@ Bestätigungen + + Neuer Umschlag + + + Umschlag laden + + + Umschlag zurĂĽckrufen/löschen + + + Daten Aktualisieren + + + Aktualisiert: {0} + + + Dokument anzeigen + + + Empfänger kontaktieren + + + Umschlag-ID: {0} + + + Ă–ffne Log Verzeichnis + + + Ergebnisbericht anzeigen + + + Support Mail + + + Einladung manuell versenden + + + Export + + + Empfänger + + + Email Anrede + + + Unterschrieben wann + + + Zugangscode + + + Benutzer + + + Typ + + + Titel + + + Erstellt am + + + Zuletzt geändert am + + + Offene Umschläge + + + Abgeschlossene Umschläge + + + Zugangscode senden + + + 2-Faktor Eigenschaften + + + Name + + + Telefonnummer + + + Empfänger hinzufĂĽgen + + + Empfänger löschen + + + Datei hinzufĂĽgen + + + Dateien zusammenfĂĽhren + + + Datei löschen + + + Datei anzeigen + + + Felder bearbeiten + + + Daten bearbeiten + + + Speichern + + + Umschlag versenden + + + Abbrechen + + + Signatur hinzufĂĽgen + + + Signatur löschen + + + Sprache + + + Zugangscode verwenden + + + 2-Faktor-Authentifizierung aktiviert + + + Zertifizierungstyp + + + Finale E-Mail an Ersteller + + + Finale E-Mail an Empfänger + + + Erinnerungs-E-Mails senden + + + Erste Erinnerung (Tage) + + + Erinnerungsintervall (Tage) + + + Läuft ab nach (Tage) + + + Ablaufwarnung (Tage) + + + Nachricht + + + Umschlagtyp + + + Alle Optionen + + + Grund fĂĽr Löschung + + + Bitte geben Sie einen Grund an + + + Status + \ No newline at end of file diff --git a/EnvelopeGenerator.Application/Resources/Resource.en-US.resx b/EnvelopeGenerator.Application/Resources/Resource.en-US.resx index fa6c5afd..0db372f4 100644 --- a/EnvelopeGenerator.Application/Resources/Resource.en-US.resx +++ b/EnvelopeGenerator.Application/Resources/Resource.en-US.resx @@ -477,4 +477,178 @@ Confirmations + + New Envelope + + + Load Envelope + + + Delete Envelope + + + Reload Data + + + Refreshed: {0} + + + Show Document + + + Contact Receiver + + + Envelope-ID: {0} + + + Open Log Directory + + + Show Results Report + + + Support Mail + + + Send Invitation Again + + + Export + + + Receivers + + + Email Salutation + + + Signed When + + + Access Code + + + User + + + Type + + + Title + + + Created On + + + Last Modified + + + Open Envelopes + + + Completed Envelopes + + + Send Access Code + + + 2-Factor Properties + + + Name + + + Phone Number + + + Add Receiver + + + Delete Receiver + + + Add File + + + Merge Files + + + Delete File + + + Show File + + + Edit Fields + + + Edit Data + + + Save + + + Send Envelope + + + Cancel + + + Add Signature + + + Delete Signature + + + Language + + + Use Access Code + + + 2-Factor Authentication Enabled + + + Certification Type + + + Final Email to Creator + + + Final Email to Receivers + + + Send Reminder Emails + + + First Reminder (Days) + + + Reminder Interval (Days) + + + Expires After (Days) + + + Expiry Warning (Days) + + + Message + + + Envelope Type + + + All Options + + + Deletion Reason + + + Please provide a reason + + + Status + \ No newline at end of file diff --git a/EnvelopeGenerator.Application/Resources/Resource.fr-FR.resx b/EnvelopeGenerator.Application/Resources/Resource.fr-FR.resx index be60d7bb..75834e1b 100644 --- a/EnvelopeGenerator.Application/Resources/Resource.fr-FR.resx +++ b/EnvelopeGenerator.Application/Resources/Resource.fr-FR.resx @@ -477,4 +477,178 @@ Confirmations + + Nouvelle enveloppe + + + Charger l'enveloppe + + + Supprimer l'enveloppe + + + Actualiser les donnĂ©es + + + ActualisĂ© : {0} + + + Afficher le document + + + Contacter le destinataire + + + ID d'enveloppe : {0} + + + Ouvrir le rĂ©pertoire des logs + + + Afficher le rapport de rĂ©sultats + + + E-mail de support + + + Renvoyer l'invitation + + + Exporter + + + Destinataires + + + Formule de politesse + + + SignĂ© quand + + + Code d'accès + + + Utilisateur + + + Type + + + Titre + + + Créé le + + + Dernière modification + + + Enveloppes ouvertes + + + Enveloppes terminĂ©es + + + Envoyer le code d'accès + + + PropriĂ©tĂ©s 2-facteurs + + + Nom + + + NumĂ©ro de tĂ©lĂ©phone + + + Ajouter un destinataire + + + Supprimer le destinataire + + + Ajouter un fichier + + + Fusionner les fichiers + + + Supprimer le fichier + + + Afficher le fichier + + + Modifier les champs + + + Modifier les donnĂ©es + + + Enregistrer + + + Envoyer l'enveloppe + + + Annuler + + + Ajouter une signature + + + Supprimer la signature + + + Langue + + + Utiliser un code d'accès + + + Authentification Ă  2 facteurs activĂ©e + + + Type de certification + + + E-mail final au crĂ©ateur + + + E-mail final aux destinataires + + + Envoyer des e-mails de rappel + + + Premier rappel (jours) + + + Intervalle de rappel (jours) + + + Expire après (jours) + + + Avertissement d'expiration (jours) + + + Message + + + Type d'enveloppe + + + Toutes les options + + + Motif de suppression + + + Veuillez indiquer une raison + + + Statut + \ No newline at end of file diff --git a/EnvelopeGenerator.Domain/Constants/EnvelopeStatus.cs b/EnvelopeGenerator.Domain/Constants/EnvelopeStatus.cs index 5242a5f8..f096fd09 100644 --- a/EnvelopeGenerator.Domain/Constants/EnvelopeStatus.cs +++ b/EnvelopeGenerator.Domain/Constants/EnvelopeStatus.cs @@ -1,8 +1,10 @@ -using System.Collections.Generic; +using System; +using System.Collections.Generic; +using System.Linq; namespace EnvelopeGenerator.Domain.Constants { - // http://wiki.dd/xwiki13/bin/view/Anwendungen/Produkt-Handbuch/Sonstiges/SignFlow/Envelope%20Status/ + // http://wiki.dd/xwiki_prod/bin/view/Anwendungen/Produkt-Handbuch/Sonstiges/signFLOW/signFLOW%20-%20Enwickler-Handbuch/4.%20Anhang/4.3%20Historie%20und%20Status%20der%20Umschl%C3%A4ge/ public enum EnvelopeStatus { Invalid = 0, @@ -49,5 +51,28 @@ namespace EnvelopeGenerator.Domain.Constants EnvelopeStatus.EnvelopeCreated, EnvelopeStatus.DocumentMod_Rotation }; + + public static readonly List Active = Enum.GetValues(typeof(EnvelopeStatus)) + .Cast() + .Where(status => status.IsActive()) + .ToList(); + + public static readonly List Completed = Enum.GetValues(typeof(EnvelopeStatus)) + .Cast() + .Where(status => status.IsCompleted()) + .ToList(); + } + + public static class EnvelopeStatusExtensions + { + public static bool IsActive(this EnvelopeStatus status) + { + return status >= EnvelopeStatus.EnvelopeCreated && status < EnvelopeStatus.EnvelopePartlySigned; + } + + public static bool IsCompleted(this EnvelopeStatus status) + { + return status >= EnvelopeStatus.EnvelopeCompletelySigned && status <= EnvelopeStatus.EnvelopeWithdrawn; + } } } \ No newline at end of file diff --git a/EnvelopeGenerator.Infrastructure/EGDbContextFactory.cs b/EnvelopeGenerator.Infrastructure/EGDbContextFactory.cs deleted file mode 100644 index fea2b764..00000000 --- a/EnvelopeGenerator.Infrastructure/EGDbContextFactory.cs +++ /dev/null @@ -1,48 +0,0 @@ -#if NET -using EnvelopeGenerator.Application.Common.Configurations; -using Microsoft.EntityFrameworkCore; -using Microsoft.EntityFrameworkCore.Design; -using Microsoft.Extensions.Configuration; -using Microsoft.Extensions.Options; - -namespace EnvelopeGenerator.Infrastructure -{ - public class EGDbContextFactory : IDesignTimeDbContextFactory - { - public EGDbContext CreateDbContext(string[] args) - { - var config = new ConfigurationBuilder() - .SetBasePath(Directory.GetCurrentDirectory()) - .AddJsonFile("appsettings.migration.json") - .Build(); - - // create DbContextOptions - var optionsBuilder = new DbContextOptionsBuilder(); - optionsBuilder.UseSqlServer(config.GetConnectionString("Default")); - - // create DbTriggerParams - var triggerLists = config.GetSection("DbTriggerParams").Get>>(); - var dbTriggerParams = new DbTriggerParams(); - if (triggerLists is not null) - foreach (var triggerList in triggerLists) - { - if (triggerList.Value.Count == 0) - continue; // Skip empty trigger lists - - var tableName = triggerList.Key; - - dbTriggerParams[tableName] = new List(); - - foreach (var trigger in triggerList.Value) - { - dbTriggerParams[tableName].Add(trigger); - } - } - - var dbContext = new EGDbContext(optionsBuilder.Options, Options.Create(dbTriggerParams)); - dbContext.IsMigration = true; - return dbContext; - } - } -} -#endif \ No newline at end of file diff --git a/EnvelopeGenerator.ReceiverUI/App.razor b/EnvelopeGenerator.ReceiverUI/App.razor index c5ac499c..3a56f120 100644 --- a/EnvelopeGenerator.ReceiverUI/App.razor +++ b/EnvelopeGenerator.ReceiverUI/App.razor @@ -1,4 +1,6 @@ - +@using System.Globalization + + @@ -7,4 +9,4 @@

Sorry, there's nothing at this address.

-
\ No newline at end of file +
diff --git a/EnvelopeGenerator.ReceiverUI/EnvelopeGenerator.ReceiverUI.csproj b/EnvelopeGenerator.ReceiverUI/EnvelopeGenerator.ReceiverUI.csproj index aa225212..c367f14c 100644 --- a/EnvelopeGenerator.ReceiverUI/EnvelopeGenerator.ReceiverUI.csproj +++ b/EnvelopeGenerator.ReceiverUI/EnvelopeGenerator.ReceiverUI.csproj @@ -24,11 +24,13 @@ - - - - + + + + + + @@ -40,6 +42,9 @@ + + + Always diff --git a/EnvelopeGenerator.ReceiverUI/Models/AnnotationDto.cs b/EnvelopeGenerator.ReceiverUI/Models/AnnotationDto.cs deleted file mode 100644 index ced9c039..00000000 --- a/EnvelopeGenerator.ReceiverUI/Models/AnnotationDto.cs +++ /dev/null @@ -1,32 +0,0 @@ -namespace EnvelopeGenerator.ReceiverUI.Models; - -/// -/// Represents a pre-assigned signature annotation position on a specific page. -///

-/// Coordinate unit (X, Y): Inches (GdPicture14 native unit), -/// origin at the top-left corner of the page, both axes increase downward/rightward. -///

-/// Conversion to DevExpress: Multiply by 100 (DX uses 1/100 inch). -/// Convert: xDX = xInches * 100.0 -///
-/// Conversion to PDF Points: Multiply by 72 (1 inch = 72 points). -/// Convert: xPt = xInches * 72.0 -///
-/// Y-axis for PDF (bottom-left origin): Flip required for iText7. -/// Convert: yPt = (pageHeightInches - yInches - elemHeightInches) * 72.0 -///
-[Obsolete("Use SignatureDto with SignatureService.")] -public record AnnotationDto -{ - /// Unique identifier of the annotation. - public long Id { get; init; } - - /// 1-based page number within the document. - public int Page { get; init; } - - /// Horizontal position in INCHES from the left edge of the page. - public double X { get; init; } - - /// Vertical position in INCHES from the top edge of the page. - public double Y { get; init; } -} diff --git a/EnvelopeGenerator.ReceiverUI/Models/EnvelopeDto.cs b/EnvelopeGenerator.ReceiverUI/Models/EnvelopeDto.cs new file mode 100644 index 00000000..eb6fb910 --- /dev/null +++ b/EnvelopeGenerator.ReceiverUI/Models/EnvelopeDto.cs @@ -0,0 +1,39 @@ +using System.Text.Json.Serialization; + +namespace EnvelopeGenerator.ReceiverUI.Models; + +public class EnvelopeDto +{ + [JsonPropertyName("id")] + public int Id { get; set; } + + [JsonPropertyName("uuid")] + public string? Uuid { get; set; } + + [JsonPropertyName("title")] + public string? Title { get; set; } + + [JsonPropertyName("status")] + public int Status { get; set; } + + [JsonPropertyName("docResult")] + public byte[]? DocResult { get; set; } + + [JsonPropertyName("envelopeReceivers")] + public List EnvelopeReceivers { get; set; } = new(); +} + +/// +/// Simplified receiver model for envelope list display +/// +public class EnvelopeReceiverSimpleDto +{ + [JsonPropertyName("name")] + public string? Name { get; set; } + + [JsonPropertyName("email")] + public string? Email { get; set; } + + [JsonPropertyName("signed")] + public bool Signed { get; set; } +} diff --git a/EnvelopeGenerator.ReceiverUI/Pages/EnvelopeReceiverPage.razor b/EnvelopeGenerator.ReceiverUI/Pages/EnvelopeReceiverPage.razor index b1525967..b58ac15b 100644 --- a/EnvelopeGenerator.ReceiverUI/Pages/EnvelopeReceiverPage.razor +++ b/EnvelopeGenerator.ReceiverUI/Pages/EnvelopeReceiverPage.razor @@ -11,7 +11,7 @@ @inject IOptions AppOptions @inject IOptions PdfViewerOptions @inject IJSRuntime JSRuntime -@inject SignatureService SignatureService +@inject DocReceiverElementService SignatureService @inject SignatureCacheService SignatureCacheService @inject EnvelopeGenerator.ReceiverUI.Services.AuthService AuthService @inject EnvelopeGenerator.ReceiverUI.Services.EnvelopeReceiverService EnvelopeReceiverService diff --git a/EnvelopeGenerator.ReceiverUI/Pages/EnvelopeSenderPage.razor b/EnvelopeGenerator.ReceiverUI/Pages/EnvelopeSenderPage.razor index 2b0b9f0e..3f9017a6 100644 --- a/EnvelopeGenerator.ReceiverUI/Pages/EnvelopeSenderPage.razor +++ b/EnvelopeGenerator.ReceiverUI/Pages/EnvelopeSenderPage.razor @@ -1,7 +1,442 @@ @page "/sender" +@attribute [Microsoft.AspNetCore.Authorization.Authorize(Policy = "Sender")] -

EnvelopeSender

+@using System.Text.Json +@using EnvelopeGenerator.Domain.Constants +@using EnvelopeGenerator.ReceiverUI.Models +@using DevExpress.Blazor +@using EnvelopeGenerator.ReceiverUI.Services +@inject EnvelopeGenerator.ReceiverUI.Services.EnvelopeService EnvelopeService +@inject EnvelopeGenerator.ReceiverUI.Services.AuthService AuthService +@inject NavigationManager Navigation +@inject IJSRuntime JSRuntime +@inject AppVersionService AppVersion + + + + + +
+
+
+
+ +
Umschlag-Ăśbersicht
+
+ +
+ + + + + + + + + +
+
+
+ +
+ @if (_isLoading && _allEnvelopes == null) { +
+
+
+ Lädt... +
+

Umschläge werden geladen...

+
+
+ } else if (_errorMessage != null) { +
+
+
+ + + + +
+
Fehler beim Laden der Umschläge
+

@_errorMessage

+
+
+
+
+ } else { +
+
+ + +
+ +
+ @if (_activeTab == "active") { + + + + + @((cellContext.DataItem as EnvelopeDto)?.Id) + + + + + @((cellContext.DataItem as EnvelopeDto)?.Title) + + + + + @{ + var envelope = cellContext.DataItem as EnvelopeDto; + if (envelope != null) { + var statusInfo = GetStatusInfo(envelope.Status); +
+ + @statusInfo.Label +
+ } + } +
+
+ + + @{ + var envelope = cellContext.DataItem as EnvelopeDto; + if (envelope != null) { + var receivers = envelope.EnvelopeReceivers ?? new List(); + var signed = receivers.Count(r => r.Signed); + var total = receivers.Count; +
+ + @signed / @total unterschrieben + + @if (total > 0) { +
+
+
+ } +
+ } + } +
+
+
+ +
+
Empfänger
+ @{ + var envelope = detailContext.DataItem as EnvelopeDto; + if (envelope?.EnvelopeReceivers?.Any() == true) { +
+ @foreach (var receiver in envelope.EnvelopeReceivers) { +
+ + @if (receiver.Signed) { + + + + Unterschrieben + } else { + + + + + Ausstehend + } + +
+ @receiver.Name + @receiver.Email +
+
+ } +
+ } else { +

Keine Empfänger

+ } + } +
+
+
+ } else { + + + + + @((cellContext.DataItem as EnvelopeDto)?.Id) + + + + + @((cellContext.DataItem as EnvelopeDto)?.Title) + + + + + @{ + var envelope = cellContext.DataItem as EnvelopeDto; + if (envelope != null) { + var statusInfo = GetStatusInfo(envelope.Status); +
+ + @statusInfo.Label +
+ } + } +
+
+ + + @{ + var envelope = cellContext.DataItem as EnvelopeDto; + if (envelope != null) { + var receivers = envelope.EnvelopeReceivers ?? new List(); + var signed = receivers.Count(r => r.Signed); + var total = receivers.Count; +
+ + @signed / @total unterschrieben + + @if (total > 0) { +
+
+
+ } +
+ } + } +
+
+
+ +
+
Empfänger
+ @{ + var envelope = detailContext.DataItem as EnvelopeDto; + if (envelope?.EnvelopeReceivers?.Any() == true) { +
+ @foreach (var receiver in envelope.EnvelopeReceivers) { +
+ + @if (receiver.Signed) { + + + + Unterschrieben + } else { + + + + + Ausstehend + } + +
+ @receiver.Name + @receiver.Email +
+
+ } +
+ } else { +

Keine Empfänger

+ } + } +
+
+
+ } +
+
+ } +
+
@code { + private IEnumerable? _allEnvelopes; + private IEnumerable? _activeEnvelopes; + private IEnumerable? _completedEnvelopes; + private EnvelopeDto? _selectedEnvelope; + private string _activeTab = "active"; + private bool _isLoading = true; + private bool _isLoggingOut = false; + private string? _errorMessage; + private DxGrid? _gridActive; + private DxGrid? _gridCompleted; -} + protected override async Task OnInitializedAsync() + { + var hasAccess = await AuthService.CheckSenderAsync(); + if (!hasAccess) + { + Navigation.NavigateTo($"/sender/login"); + return; + } + + await LoadEnvelopesAsync(); + } + + async Task LoadEnvelopesAsync() + { + _isLoading = true; + _errorMessage = null; + await InvokeAsync(StateHasChanged); + + try + { + _allEnvelopes = await EnvelopeService.GetAsync() ?? []; + + // Split into active and completed based on status + var envelopes = _allEnvelopes.ToList(); + _activeEnvelopes = envelopes.Where(e => ((EnvelopeStatus)e.Status).IsActive()).ToList(); + _completedEnvelopes = envelopes.Where(e => ((EnvelopeStatus)e.Status).IsCompleted()).ToList(); + + await JSRuntime.InvokeVoidAsync("console.log", $"Loaded {_activeEnvelopes.Count()} active and {_completedEnvelopes.Count()} completed envelopes"); + } + catch (Exception ex) + { + _errorMessage = ex.Message; + await JSRuntime.InvokeVoidAsync("console.error", "Fehler beim Laden der Umschläge:", ex.ToString()); + } + finally + { + _isLoading = false; + await InvokeAsync(StateHasChanged); + } + } + + async Task RefreshEnvelopes() + { + await LoadEnvelopesAsync(); + } + + void CreateEnvelope() + { + // TODO: Navigate to envelope creation page + JSRuntime.InvokeVoidAsync("console.log", "Create envelope clicked - not yet implemented"); + } + + void EditEnvelope() + { + if (_selectedEnvelope == null) return; + // TODO: Navigate to envelope editor + JSRuntime.InvokeVoidAsync("console.log", $"Edit envelope {_selectedEnvelope.Id} clicked - not yet implemented"); + } + + void DeleteEnvelope() + { + if (_selectedEnvelope == null) return; + // TODO: Show delete confirmation dialog + JSRuntime.InvokeVoidAsync("console.log", $"Delete envelope {_selectedEnvelope.Id} clicked - not yet implemented"); + } + + async Task LogoutAsync() + { + _isLoggingOut = true; + await InvokeAsync(StateHasChanged); + await AuthService.LogoutSenderAsync(); + Navigation.NavigateTo("/sender/login", forceLoad: true); + } + + bool IsEnvelopeSent(EnvelopeDto envelope) + { + var status = (EnvelopeStatus)envelope.Status; + return status >= EnvelopeStatus.EnvelopeQueued; + } + + (string Label, string CssClass, string DotColor) GetStatusInfo(int statusCode) + { + var status = (EnvelopeStatus)statusCode; + return status switch + { + EnvelopeStatus.EnvelopePartlySigned => ("Teilweise unterschrieben", "partly-signed", "green"), + EnvelopeStatus.EnvelopeQueued => ("In Warteschlange", "queued", "orange"), + EnvelopeStatus.EnvelopeSent => ("Gesendet", "sent", "orange"), + EnvelopeStatus.EnvelopeCompletelySigned => ("Vollständig unterschrieben", "completed", "green"), + EnvelopeStatus.EnvelopeDeleted => ("Gelöscht", "deleted", "red"), + EnvelopeStatus.EnvelopeRejected => ("Abgelehnt", "rejected", "red"), + EnvelopeStatus.EnvelopeWithdrawn => ("Zurückgezogen", "withdrawn", "red"), + EnvelopeStatus.EnvelopeCreated => ("Erstellt", "created", "blue"), + EnvelopeStatus.EnvelopeSaved => ("Gespeichert", "saved", "blue"), + _ => ("Unbekannt", "unknown", "blue") + }; + } + + void OnCustomizeElement(GridCustomizeElementEventArgs e) + { + // Future: Add custom row coloring based on status if needed + } + + void OnSelectedEnvelopeChanged(object envelope) + { + _selectedEnvelope = envelope as EnvelopeDto; + } +} \ No newline at end of file diff --git a/EnvelopeGenerator.ReceiverUI/Pages/Example/ReportViewer.razor b/EnvelopeGenerator.ReceiverUI/Pages/Example/ReportViewer.razor index 6e5f275e..42bfc0d7 100644 --- a/EnvelopeGenerator.ReceiverUI/Pages/Example/ReportViewer.razor +++ b/EnvelopeGenerator.ReceiverUI/Pages/Example/ReportViewer.razor @@ -6,6 +6,7 @@ @using DevExpress.Utils @using DevExpress.XtraPrinting @using DevExpress.XtraPrinting.Drawing +@using EnvelopeGenerator.Application.Common.Dto @using Microsoft.JSInterop @using XtraReport = DevExpress.XtraReports.UI.XtraReport @using BottomMarginBand = DevExpress.XtraReports.UI.BottomMarginBand @@ -301,7 +302,10 @@ Shown="OnPopupShownAsync"> bool IsLoggingOut; IReadOnlyList _annotations = []; - IEnumerable AnnotationPages => _annotations.Select(a => a.Page).Distinct().OrderBy(p => p); + IEnumerable AnnotationPages => _annotations + .Select(a => a.Page ?? throw new InvalidOperationException($"Annotation page is missing for annotation ID {a.Id}. Annotation details: X={a.X}, Y={a.Y}")) + .Distinct() + .OrderBy(p => p); EnvelopeReceiverDto? _envelopeReceiver; record SignatureCapture(string DataUrl, string FullName, string Position, string Place); SignatureCapture? _capturedSignature; diff --git a/EnvelopeGenerator.ReceiverUI/Program.cs b/EnvelopeGenerator.ReceiverUI/Program.cs index 1c825e94..19ac86f9 100644 --- a/EnvelopeGenerator.ReceiverUI/Program.cs +++ b/EnvelopeGenerator.ReceiverUI/Program.cs @@ -7,6 +7,9 @@ using EnvelopeGenerator.ReceiverUI.Options; using DevExpress.XtraReports.Services; using DevExpress.Blazor.Reporting; using DevExpress.XtraReports.Web.Extensions; +using EnvelopeGenerator.Application.Resources; +using Microsoft.Extensions.Localization; +using System.Globalization; var builder = WebAssemblyHostBuilder.CreateDefault(args); builder.RootComponents.Add("#app"); @@ -20,9 +23,14 @@ builder.Services.AddScoped(); builder.Services.AddScoped(); builder.Services.AddScoped(); builder.Services.AddScoped(); -builder.Services.AddScoped(); +builder.Services.AddScoped(); builder.Services.AddScoped(); builder.Services.AddSingleton(); +builder.Services.AddScoped(); +builder.Services.AddScoped(); + +// Localization services +builder.Services.AddLocalization(); builder.Services.AddDevExpressWebAssemblyBlazorReportViewer(); builder.Services.AddDevExpressWebAssemblyBlazorPdfViewer(); @@ -41,5 +49,30 @@ builder.Services.AddScoped(); ReportStorageWebExtension.RegisterExtensionGlobal(new InMemoryReportStorageWebExtension()); var host = builder.Build(); + +// ⚠️ IMPORTANT: BLAZOR WASM-SPECIFIC CULTURE INITIALIZATION +// This approach sets DefaultThreadCurrentCulture globally, which is SAFE for WebAssembly +// because each user runs their own isolated app instance in their browser. +// +// ⚠️ TODO: REMOVE/REFACTOR WHEN MIGRATING TO BLAZOR SERVER/AUTO +// In Server/Auto render modes, this is DANGEROUS because: +// - Server runs a single shared instance for all users +// - Setting global culture affects ALL connected users simultaneously +// - Race conditions and culture conflicts will occur +// +// Migration Guide: +// - Option 1: Use RequestLocalizationMiddleware for per-request culture +// - Option 2: Use CascadingParameter with per-circuit culture state +// - See: https://learn.microsoft.com/aspnet/core/blazor/globalization-localization +// +// Related files to update on migration: +// - LanguageSelector.razor (remove manual culture setting) +// - App.razor (may need CascadingValue for culture) +// - Startup/Program.cs (add middleware) +var cultureService = host.Services.GetRequiredService(); +var culture = await cultureService.InitializeCultureAsync(); +CultureInfo.DefaultThreadCurrentCulture = culture; +CultureInfo.DefaultThreadCurrentUICulture = culture; + await FontLoader.LoadFonts(host.Services.GetRequiredService(), new List { "opensans.ttf" }); await host.RunAsync(); diff --git a/EnvelopeGenerator.ReceiverUI/Properties/launchSettings.json b/EnvelopeGenerator.ReceiverUI/Properties/launchSettings.json index 166bbda2..227c2ec4 100644 --- a/EnvelopeGenerator.ReceiverUI/Properties/launchSettings.json +++ b/EnvelopeGenerator.ReceiverUI/Properties/launchSettings.json @@ -2,7 +2,7 @@ "profiles": { "EnvelopeGenerator.ReceiverUI": { "commandName": "Project", - "launchBrowser": true, + "launchBrowser": false, "environmentVariables": { "ASPNETCORE_ENVIRONMENT": "Development" }, diff --git a/EnvelopeGenerator.ReceiverUI/Services/AnnotationService.cs b/EnvelopeGenerator.ReceiverUI/Services/AnnotationService.cs index ead7437c..74bb3960 100644 --- a/EnvelopeGenerator.ReceiverUI/Services/AnnotationService.cs +++ b/EnvelopeGenerator.ReceiverUI/Services/AnnotationService.cs @@ -1,5 +1,6 @@ using System.Net.Http.Json; using System.Text.Json; +using EnvelopeGenerator.Application.Common.Dto; using EnvelopeGenerator.ReceiverUI.Models; using EnvelopeGenerator.ReceiverUI.Options; using Microsoft.Extensions.Options; diff --git a/EnvelopeGenerator.ReceiverUI/Services/AuthService.cs b/EnvelopeGenerator.ReceiverUI/Services/AuthService.cs index c0b020dd..9bff4bd1 100644 --- a/EnvelopeGenerator.ReceiverUI/Services/AuthService.cs +++ b/EnvelopeGenerator.ReceiverUI/Services/AuthService.cs @@ -58,6 +58,16 @@ public class AuthService(HttpClient http, IOptions apiOptions) return response.IsSuccessStatusCode; } + /// + /// Checks whether the current user holds a valid receiver token for the given envelope key. + /// Calls GET /api/auth/check/envelope/{envelopeKey}. + /// + public async Task CheckSenderAsync(CancellationToken cancel = default) + { + var response = await http.GetAsync($"{_api.BaseUrl}/api/auth/check", cancel); + return response.StatusCode == HttpStatusCode.OK; + } + /// /// Authenticates a sender user with username and password. /// Calls POST /api/auth?cookie=true with JSON body. @@ -78,4 +88,16 @@ public class AuthService(HttpClient http, IOptions apiOptions) _ => SenderLoginResult.Error }; } + + /// + /// Logs out the sender user by removing the authentication cookie. + /// Calls POST /api/auth/logout. + /// + public async Task LogoutSenderAsync(CancellationToken cancel = default) + { + var response = await http.PostAsync( + $"{_api.BaseUrl}/api/auth/logout", + null, cancel); + return response.IsSuccessStatusCode; + } } diff --git a/EnvelopeGenerator.ReceiverUI/Services/CultureService.cs b/EnvelopeGenerator.ReceiverUI/Services/CultureService.cs new file mode 100644 index 00000000..2b4e4aeb --- /dev/null +++ b/EnvelopeGenerator.ReceiverUI/Services/CultureService.cs @@ -0,0 +1,74 @@ +using System.Globalization; +using Microsoft.JSInterop; + +namespace EnvelopeGenerator.ReceiverUI.Services; + +/// +/// Service for managing application culture/localization. +/// +public class CultureService +{ + private readonly IJSRuntime _jsRuntime; + private const string CULTURE_KEY = "AppCulture"; + + public CultureService(IJSRuntime jsRuntime) + { + _jsRuntime = jsRuntime; + } + + /// + /// Gets the list of supported cultures. + /// + public static CultureInfo[] SupportedCultures { get; } = new[] + { + new CultureInfo("de-DE"), + new CultureInfo("en-US"), + new CultureInfo("fr-FR") + }; + + /// + /// Sets the application culture and stores it in localStorage. + /// + public async Task SetCultureAsync(string culture) + { + if (!SupportedCultures.Any(c => c.Name == culture)) + throw new ArgumentException($"Culture '{culture}' is not supported.", nameof(culture)); + + await _jsRuntime.InvokeVoidAsync("localStorage.setItem", CULTURE_KEY, culture); + } + + /// + /// Gets the stored culture from localStorage. + /// + public async Task GetCultureAsync() + { + try + { + return await _jsRuntime.InvokeAsync("localStorage.getItem", CULTURE_KEY); + } + catch + { + return null; + } + } + + /// + /// Initializes the culture from localStorage or browser settings. + /// + public async Task InitializeCultureAsync() + { + var storedCulture = await GetCultureAsync(); + + if (!string.IsNullOrEmpty(storedCulture) && + SupportedCultures.Any(c => c.Name == storedCulture)) + { + return new CultureInfo(storedCulture); + } + + // Fallback to browser culture or default + var browserCulture = CultureInfo.CurrentCulture.Name; + var matchedCulture = SupportedCultures.FirstOrDefault(c => c.Name == browserCulture); + + return matchedCulture ?? SupportedCultures[0]; // Default to German + } +} diff --git a/EnvelopeGenerator.ReceiverUI/Services/SignatureService.cs b/EnvelopeGenerator.ReceiverUI/Services/DocReceiverElementService.cs similarity index 81% rename from EnvelopeGenerator.ReceiverUI/Services/SignatureService.cs rename to EnvelopeGenerator.ReceiverUI/Services/DocReceiverElementService.cs index 0955ed5b..34ef1916 100644 --- a/EnvelopeGenerator.ReceiverUI/Services/SignatureService.cs +++ b/EnvelopeGenerator.ReceiverUI/Services/DocReceiverElementService.cs @@ -6,13 +6,13 @@ using Microsoft.Extensions.Options; namespace EnvelopeGenerator.ReceiverUI.Services; -public class SignatureService(HttpClient http, IOptions apiOptions) +public class DocReceiverElementService(HttpClient http, IOptions apiOptions) { private static readonly JsonSerializerOptions _jsonOptions = new(JsonSerializerDefaults.Web); public async Task> GetAsync(string envelopeKey, CancellationToken cancel = default) { - var url = $"{apiOptions.Value.BaseUrl}/api/Signature/{Uri.EscapeDataString(envelopeKey)}"; + var url = $"{apiOptions.Value.BaseUrl}/api/DocReceiverElement/{Uri.EscapeDataString(envelopeKey)}"; var response = await http.GetAsync(url, cancel); if (!response.IsSuccessStatusCode) diff --git a/EnvelopeGenerator.ReceiverUI/Services/EnvelopeService.cs b/EnvelopeGenerator.ReceiverUI/Services/EnvelopeService.cs new file mode 100644 index 00000000..2e61f47d --- /dev/null +++ b/EnvelopeGenerator.ReceiverUI/Services/EnvelopeService.cs @@ -0,0 +1,72 @@ +using System.Net.Http.Json; +using System.Text.Json; +using EnvelopeGenerator.ReceiverUI.Models; +using EnvelopeGenerator.ReceiverUI.Options; +using Microsoft.AspNetCore.WebUtilities; +using Microsoft.Extensions.Options; + +namespace EnvelopeGenerator.ReceiverUI.Services; + +/// +/// Retrieves s from the API. +/// +public class EnvelopeService +{ + private readonly HttpClient _http; + private readonly ApiOptions _apiOptions; + private static readonly JsonSerializerOptions _jsonOptions = new(JsonSerializerDefaults.Web); + + public EnvelopeService(HttpClient http, IOptions apiOptions) + { + _http = http; + _apiOptions = apiOptions.Value; + } + + /// + /// Fetches envelopes from the API with optional filters. + /// + /// Thrown when the API request fails. + public async Task?> GetAsync( + int? id = null, + string? uuid = null, + bool? onlyActive = null, + bool? onlyCompleted = null, + CancellationToken cancel = default) + { + var baseUrl = $"{_apiOptions.BaseUrl}/api/Envelope"; + var queryParams = new Dictionary(); + + if (id.HasValue) + { + queryParams["Id"] = id.Value.ToString(); + } + if (!string.IsNullOrEmpty(uuid)) + { + queryParams["Uuid"] = uuid; + } + if (onlyActive.HasValue) + { + queryParams["OnlyActive"] = onlyActive.Value.ToString(); + } + if (onlyCompleted.HasValue) + { + queryParams["OnlyCompleted"] = onlyCompleted.Value.ToString(); + } + + var url = QueryHelpers.AddQueryString(baseUrl, queryParams); + + var response = await _http.GetAsync(url, cancel); + + if (!response.IsSuccessStatusCode) + { + var statusCode = (int)response.StatusCode; + var reasonPhrase = response.ReasonPhrase ?? "Unknown error"; + throw new HttpRequestException( + $"Failed to load envelopes. Status: {statusCode} ({reasonPhrase})", + null, + response.StatusCode); + } + + return await response.Content.ReadFromJsonAsync>(_jsonOptions, cancel); + } +} diff --git a/EnvelopeGenerator.ReceiverUI/Shared/LanguageSelector.razor b/EnvelopeGenerator.ReceiverUI/Shared/LanguageSelector.razor new file mode 100644 index 00000000..e426e22a --- /dev/null +++ b/EnvelopeGenerator.ReceiverUI/Shared/LanguageSelector.razor @@ -0,0 +1,80 @@ +@using System.Globalization +@using EnvelopeGenerator.ReceiverUI.Services +@inject IJSRuntime JSRuntime +@inject NavigationManager Navigation +@inject CultureService CultureService + +
+ + + @if (isOpen) + { +
+ + + +
+ } +
+ +@code { + private bool isOpen = false; + private string CurrentCulture => CultureInfo.CurrentCulture.Name; + + private void ToggleDropdown() + { + isOpen = !isOpen; + } + + private async Task ChangeLanguageAsync(string culture) + { + if (CultureInfo.CurrentCulture.Name != culture) + { + await CultureService.SetCultureAsync(culture); + + // Set culture without page reload + var cultureInfo = new CultureInfo(culture); + CultureInfo.DefaultThreadCurrentCulture = cultureInfo; + CultureInfo.DefaultThreadCurrentUICulture = cultureInfo; + + // Navigate without reload to trigger re-render + Navigation.NavigateTo(Navigation.Uri, forceLoad: false); + } + + isOpen = false; + } + + private string GetFlagCode(string culture) + { + return culture switch + { + "de-DE" => "de", + "en-US" => "us", + "fr-FR" => "fr", + _ => "de" + }; + } + + private string GetLanguageName(string culture) + { + return culture switch + { + "de-DE" => "Deutsch", + "en-US" => "English", + "fr-FR" => "Français", + _ => "Deutsch" + }; + } +} diff --git a/EnvelopeGenerator.ReceiverUI/Shared/LanguageSelector.razor.css b/EnvelopeGenerator.ReceiverUI/Shared/LanguageSelector.razor.css new file mode 100644 index 00000000..e69de29b diff --git a/EnvelopeGenerator.ReceiverUI/Shared/MainLayout.razor b/EnvelopeGenerator.ReceiverUI/Shared/MainLayout.razor index e667fbac..873ea018 100644 --- a/EnvelopeGenerator.ReceiverUI/Shared/MainLayout.razor +++ b/EnvelopeGenerator.ReceiverUI/Shared/MainLayout.razor @@ -8,9 +8,12 @@ diff --git a/EnvelopeGenerator.ReceiverUI/Shared/MainLayout.razor.css b/EnvelopeGenerator.ReceiverUI/Shared/MainLayout.razor.css index d662d7ea..ef51f3a9 100644 --- a/EnvelopeGenerator.ReceiverUI/Shared/MainLayout.razor.css +++ b/EnvelopeGenerator.ReceiverUI/Shared/MainLayout.razor.css @@ -15,3 +15,18 @@ article { padding: 0 !important; margin: 0 !important; } + +.receiver-footer { + display: flex; + justify-content: space-between; + align-items: center; + padding: 1rem 2rem; + gap: 1rem; +} + +.receiver-footer__content { + display: flex; + align-items: center; + gap: 0.5rem; +} + diff --git a/EnvelopeGenerator.ReceiverUI/wwwroot/css/app.css b/EnvelopeGenerator.ReceiverUI/wwwroot/css/app.css index 9f967169..48de5029 100644 --- a/EnvelopeGenerator.ReceiverUI/wwwroot/css/app.css +++ b/EnvelopeGenerator.ReceiverUI/wwwroot/css/app.css @@ -365,4 +365,75 @@ article { .receiver-footer__sep { opacity: 0.4; -} \ No newline at end of file +} + +/* ── Language Selector (Footer) ──────────────────────────────────────────── */ +.language-selector { + position: relative; + display: inline-block; +} + +.language-selector__trigger { + display: flex; + align-items: center; + gap: 0.4rem; + padding: 0.25rem 0.5rem; + background: rgba(255, 255, 255, 0.1); + border: 1px solid rgba(255, 255, 255, 0.2); + border-radius: 4px; + color: rgba(255, 255, 255, 0.9); + cursor: pointer; + font-size: 1rem; + transition: all 0.2s ease; +} + +.language-selector__trigger:hover { + background: rgba(255, 255, 255, 0.15); + border-color: rgba(255, 255, 255, 0.3); +} + +.language-selector__arrow { + font-size: 0.6rem; + transition: transform 0.2s ease; + opacity: 0.7; +} + +.language-selector__dropdown { + position: absolute; + bottom: calc(100% + 0.5rem); + right: 0; + background: white; + border: 1px solid #ddd; + border-radius: 6px; + box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15); + min-width: 160px; + z-index: 1000; + overflow: hidden; +} + +.language-selector__option { + display: flex; + align-items: center; + gap: 0.75rem; + width: 100%; + padding: 0.65rem 1rem; + background: white; + border: none; + color: #333; + cursor: pointer; + font-size: 0.9rem; + transition: background-color 0.2s ease; + text-align: left; +} + +.language-selector__option:hover { + background-color: #f5f5f5; +} + +.language-selector__option .fi { + font-size: 1.25rem; +} + +.language-selector__option span:last-child { + font-weight: 500; +} diff --git a/EnvelopeGenerator.ReceiverUI/wwwroot/css/sender-page.css b/EnvelopeGenerator.ReceiverUI/wwwroot/css/sender-page.css new file mode 100644 index 00000000..9195af48 --- /dev/null +++ b/EnvelopeGenerator.ReceiverUI/wwwroot/css/sender-page.css @@ -0,0 +1,297 @@ +.sender-dashboard-layout { + display: flex; + flex-direction: column; + height: 100vh; + overflow: hidden; + background: linear-gradient(135deg, #1e3c72 0%, #2a5298 50%, #7e22ce 100%); +} + +.sender-action-bar { + background: rgba(255, 255, 255, 0.95); + backdrop-filter: blur(20px); + border-bottom: 3px solid rgba(126, 34, 206, 0.3); + padding: 1rem 2rem; + flex-shrink: 0; + box-shadow: 0 4px 20px rgba(0, 0, 0, 0.1); +} + +.sender-action-bar__inner { + max-width: 1600px; + margin: 0 auto; + display: flex; + align-items: center; + justify-content: space-between; + gap: 1.5rem; +} + +.sender-title-section { + display: flex; + align-items: center; + gap: 1rem; +} + +.sender-logo svg { + filter: drop-shadow(0 2px 4px rgba(126, 34, 206, 0.3)); + color: #7e22ce; +} + +.sender-title { + font-size: 1.25rem; + font-weight: 700; + color: #1e293b; + letter-spacing: -0.025em; +} + +.sender-toolbar { + display: flex; + align-items: center; + gap: 0.75rem; + flex-wrap: wrap; +} + +.sender-btn { + display: flex; + align-items: center; + gap: 0.5rem; + padding: 0.625rem 1.125rem; + background: linear-gradient(135deg, rgba(126, 34, 206, 0.05) 0%, rgba(42, 82, 152, 0.05) 100%); + border: 1px solid rgba(126, 34, 206, 0.2); + border-radius: 8px; + font-size: 0.875rem; + font-weight: 600; + color: #1e293b; + cursor: pointer; + transition: all 0.2s ease; + white-space: nowrap; +} + + .sender-btn:hover:not(:disabled) { + background: linear-gradient(135deg, rgba(126, 34, 206, 0.1) 0%, rgba(42, 82, 152, 0.1) 100%); + border-color: rgba(126, 34, 206, 0.4); + transform: translateY(-1px); + box-shadow: 0 4px 12px rgba(126, 34, 206, 0.2); + } + + .sender-btn:disabled { + opacity: 0.4; + cursor: not-allowed; + background: rgba(0, 0, 0, 0.02); + border-color: rgba(0, 0, 0, 0.1); + } + +.sender-btn--primary { + background: linear-gradient(135deg, #7e22ce 0%, #2a5298 100%); + border-color: transparent; + color: white; +} + + .sender-btn--primary:hover:not(:disabled) { + background: linear-gradient(135deg, #6b1cb0 0%, #1e3a72 100%); + transform: translateY(-1px); + box-shadow: 0 4px 16px rgba(126, 34, 206, 0.3); + } + +.sender-btn--danger { + background: linear-gradient(135deg, rgba(239, 68, 68, 0.08) 0%, rgba(220, 38, 38, 0.08) 100%); + border-color: rgba(239, 68, 68, 0.3); + color: #dc2626; +} + + .sender-btn--danger:hover:not(:disabled) { + background: linear-gradient(135deg, #ef4444 0%, #dc2626 100%); + border-color: transparent; + color: white; + } + +.sender-btn--logout { + padding: 0.5rem; + min-width: 38px; +} + +.sender-content { + flex: 1; + min-height: 0; + padding: 1.5rem; + position: relative; + overflow: auto; +} + +.sender-grid-container { + background: rgba(255, 255, 255, 0.98); + backdrop-filter: blur(20px); + border-radius: 16px; + box-shadow: 0 25px 50px -12px rgba(0, 0, 0, 0.25), 0 0 0 1px rgba(255, 255, 255, 0.1); + overflow: hidden; + position: relative; + max-width: 1600px; + margin: 0 auto; +} + + .sender-grid-container::before { + content: ''; + position: absolute; + top: 0; + left: 0; + right: 0; + height: 4px; + background: linear-gradient(90deg, #7e22ce 0%, #2a5298 100%); + z-index: 1; + border-radius: 16px 16px 0 0; + } + +.sender-tabs { + display: flex; + border-bottom: 2px solid rgba(126, 34, 206, 0.1); + padding: 0 2rem; + background: rgba(126, 34, 206, 0.02); +} + +.sender-tab { + padding: 1rem 1.5rem; + font-size: 0.875rem; + font-weight: 600; + color: #6b7280; + background: transparent; + border: none; + border-bottom: 3px solid transparent; + cursor: pointer; + transition: all 0.2s ease; + white-space: nowrap; +} + + .sender-tab:hover { + color: #7e22ce; + background: rgba(126, 34, 206, 0.05); + } + +.sender-tab--active { + color: #7e22ce; + border-bottom-color: #7e22ce; + background: white; +} + +.sender-grid-wrapper { + padding: 1.5rem 2rem 2rem; +} + +/* Hide DevExpress empty cells */ +.dxbl-grid-empty-cell { + display: none !important; +} + +.status-badge { + display: inline-flex; + align-items: center; + gap: 0.375rem; + padding: 0.25rem 0.625rem; + border-radius: 6px; + font-size: 0.75rem; + font-weight: 600; + white-space: nowrap; +} + +.status-badge--partly-signed, +.status-badge--completed { + background: rgba(129, 199, 132, 0.15); + color: #2e7d32; +} + +.status-badge--queued, +.status-badge--sent { + background: rgba(255, 183, 77, 0.15); + color: #e65100; +} + +.status-badge--deleted, +.status-badge--rejected, +.status-badge--withdrawn { + background: rgba(229, 115, 115, 0.15); + color: #c62828; +} + +.status-badge--created, +.status-badge--saved { + background: rgba(100, 181, 246, 0.15); + color: #1565c0; +} + +.status-dot { + width: 6px; + height: 6px; + border-radius: 50%; +} + +.status-dot--green { + background: #81c784; +} + +.status-dot--orange { + background: #ffb74d; +} + +.status-dot--red { + background: #e57373; +} + +.status-dot--blue { + background: #64b5f6; +} + +.receiver-badge { + display: inline-flex; + align-items: center; + gap: 0.25rem; + padding: 0.125rem 0.5rem; + background: #f3f4f6; + border-radius: 4px; + font-size: 0.75rem; + color: #374151; + white-space: nowrap; +} + +.receiver-badge--signed { + background: rgba(129, 199, 132, 0.15); + color: #2e7d32; +} + +.receiver-badge--unsigned { + background: rgba(229, 115, 115, 0.15); + color: #c62828; +} + +@@media (max-width: 768px) { + .sender-action-bar { + padding: 1rem 1.25rem; + } + + .sender-action-bar__inner { + flex-wrap: wrap; + } + + .sender-toolbar { + width: 100%; + justify-content: flex-start; + } + + .sender-title { + font-size: 1.125rem; + } + + .sender-content { + padding: 0.75rem; + } + + .sender-grid-wrapper { + padding: 1rem; + } + + .sender-tabs { + padding: 0 1rem; + overflow-x: auto; + } + + .sender-tab { + padding: 0.875rem 1rem; + font-size: 0.813rem; + } +} diff --git a/EnvelopeGenerator.ReceiverUI/wwwroot/index.html b/EnvelopeGenerator.ReceiverUI/wwwroot/index.html index ce8ae2fe..ec48b8fa 100644 --- a/EnvelopeGenerator.ReceiverUI/wwwroot/index.html +++ b/EnvelopeGenerator.ReceiverUI/wwwroot/index.html @@ -12,6 +12,7 @@ +