Compare commits
9 Commits
feat/blazo
...
affdc44f91
| Author | SHA1 | Date | |
|---|---|---|---|
| affdc44f91 | |||
| 8adc8683b8 | |||
| 4d91b0a4fb | |||
| e62cdc9d9d | |||
| 12a0974efe | |||
| 367850fee5 | |||
| 09cc639466 | |||
| c3730d109b | |||
| f510cfb5ad |
@@ -1,6 +1,5 @@
|
||||
using DigitalData.Core.Abstraction.Application.DTO;
|
||||
using DigitalData.Core.Exceptions;
|
||||
using EnvelopeGenerator.Application.Common.Dto;
|
||||
using EnvelopeGenerator.Application.Common.Extensions;
|
||||
using EnvelopeGenerator.Application.Common.Interfaces.Services;
|
||||
using EnvelopeGenerator.Application.Common.Notifications.DocSigned;
|
||||
@@ -8,13 +7,11 @@ using EnvelopeGenerator.Application.EnvelopeReceivers.Queries;
|
||||
using EnvelopeGenerator.Application.Histories.Queries;
|
||||
using EnvelopeGenerator.Domain.Constants;
|
||||
using EnvelopeGenerator.API.Extensions;
|
||||
using EnvelopeGenerator.API.Models;
|
||||
using MediatR;
|
||||
using Microsoft.AspNetCore.Authentication;
|
||||
using Microsoft.AspNetCore.Authentication.Cookies;
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using System.Dynamic;
|
||||
|
||||
namespace EnvelopeGenerator.API.Controllers;
|
||||
|
||||
@@ -118,127 +115,4 @@ public class AnnotationController : ControllerBase
|
||||
_logger.LogNotice(histRes.Notices);
|
||||
return StatusCode(500, histRes.Messages);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Returns the signature placeholders (id, page, x, y, width, height)
|
||||
/// the authenticated receiver must sign on the current envelope.
|
||||
/// Used by the Blazor receiver UI to render a custom signature overlay
|
||||
/// on top of the DevExpress PDF viewer.
|
||||
/// </summary>
|
||||
[Authorize(Policy = AuthPolicy.Receiver)]
|
||||
[HttpGet("elements")]
|
||||
public async Task<IActionResult> GetElements(CancellationToken cancel)
|
||||
{
|
||||
var signature = User.GetReceiverSignatureOfReceiver();
|
||||
var uuid = User.GetEnvelopeUuidOfReceiver();
|
||||
|
||||
var envelopeReceiver = await _mediator.ReadEnvelopeReceiverAsync(uuid, signature, cancel)
|
||||
?? throw new NotFoundException("Envelope receiver is not found.");
|
||||
|
||||
var elements = envelopeReceiver.Envelope?.Documents?.FirstOrDefault()?.Elements
|
||||
?? Enumerable.Empty<Application.Common.Dto.SignatureDto>();
|
||||
|
||||
// Only expose what the overlay needs (no internal columns).
|
||||
var payload = elements
|
||||
.Where(e => e.ReceiverId == envelopeReceiver.ReceiverId)
|
||||
.Select(e => new
|
||||
{
|
||||
id = e.Id,
|
||||
page = e.Page,
|
||||
x = e.X,
|
||||
y = e.Y,
|
||||
width = e.Width,
|
||||
height = e.Height,
|
||||
required = e.Required,
|
||||
tooltip = e.Tooltip,
|
||||
});
|
||||
|
||||
return Ok(payload);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Signs the document for the authenticated receiver using a Blazor /
|
||||
/// DevExpress friendly payload (one PNG image per placeholder, plus
|
||||
/// optional position / city per placeholder).
|
||||
///
|
||||
/// Internally produces a <see cref="PsPdfKitAnnotation"/> wrapping a
|
||||
/// list of <see cref="AnnotationCreateDto"/> entries so that the
|
||||
/// existing notification pipeline (mail / status / annotation persistence)
|
||||
/// is fully reused. The Instant JSON is left blank because the Blazor
|
||||
/// pipeline does not depend on PSPDFKit-side rendering.
|
||||
/// </summary>
|
||||
[Authorize(Policy = AuthPolicy.Receiver)]
|
||||
[HttpPost("blazor")]
|
||||
public async Task<IActionResult> SignBlazor([FromBody] BlazorSignaturePayload payload, CancellationToken cancel)
|
||||
{
|
||||
if (payload is null || payload.Signatures.Count == 0)
|
||||
return BadRequest();
|
||||
|
||||
var signature = User.GetReceiverSignatureOfReceiver();
|
||||
var uuid = User.GetEnvelopeUuidOfReceiver();
|
||||
|
||||
var envelopeReceiver = await _mediator.ReadEnvelopeReceiverAsync(uuid, signature, cancel)
|
||||
?? throw new NotFoundException("Envelope receiver is not found.");
|
||||
|
||||
if (await _mediator.IsSignedAsync(uuid, signature, cancel))
|
||||
return Problem(statusCode: StatusCodes.Status409Conflict);
|
||||
|
||||
if (await _mediator.AnyHistoryAsync(uuid, new[] { EnvelopeStatus.EnvelopeRejected, EnvelopeStatus.DocumentRejected }, cancel))
|
||||
return Problem(statusCode: StatusCodes.Status423Locked);
|
||||
|
||||
// Build a structured AnnotationCreateDto list out of the lightweight
|
||||
// payload. One DTO per (element, kind) tuple, mirroring how the legacy
|
||||
// PSPDFKit pipeline produced multiple form fields per placeholder.
|
||||
var structured = new List<AnnotationCreateDto>();
|
||||
foreach (var entry in payload.Signatures)
|
||||
{
|
||||
structured.Add(new AnnotationCreateDto
|
||||
{
|
||||
ElementId = entry.ElementId,
|
||||
Name = $"{entry.ElementId}#signature",
|
||||
Type = "signature",
|
||||
Value = entry.SignatureDataUrl,
|
||||
});
|
||||
if (!string.IsNullOrWhiteSpace(entry.Position))
|
||||
{
|
||||
structured.Add(new AnnotationCreateDto
|
||||
{
|
||||
ElementId = entry.ElementId,
|
||||
Name = $"{entry.ElementId}#position",
|
||||
Type = "position",
|
||||
Value = entry.Position!,
|
||||
});
|
||||
}
|
||||
if (!string.IsNullOrWhiteSpace(entry.City))
|
||||
{
|
||||
structured.Add(new AnnotationCreateDto
|
||||
{
|
||||
ElementId = entry.ElementId,
|
||||
Name = $"{entry.ElementId}#city",
|
||||
Type = "city",
|
||||
Value = entry.City!,
|
||||
});
|
||||
}
|
||||
structured.Add(new AnnotationCreateDto
|
||||
{
|
||||
ElementId = entry.ElementId,
|
||||
Name = $"{entry.ElementId}#date",
|
||||
Type = "date",
|
||||
Value = entry.SignedAt.ToString("o"),
|
||||
});
|
||||
}
|
||||
|
||||
// No PSPDFKit Instant JSON for the Blazor flow — pass an empty object.
|
||||
var psPdfKitAnnotation = new PsPdfKitAnnotation(new ExpandoObject(), structured);
|
||||
|
||||
var docSignedNotification = await _mediator
|
||||
.ReadEnvelopeReceiverAsync(uuid, signature, cancel)
|
||||
.ToDocSignedNotification(psPdfKitAnnotation)
|
||||
?? throw new NotFoundException("Envelope receiver is not found.");
|
||||
|
||||
await _mediator.PublishSafely(docSignedNotification, cancel);
|
||||
await HttpContext.SignOutAsync(CookieAuthenticationDefaults.AuthenticationScheme);
|
||||
|
||||
return Ok();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,407 +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 (Light-Query: ohne Documents/Elements) ──
|
||||
var er = await _mediator.Send(
|
||||
new ReadEnvelopeReceiverLightQuery { Key = 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."
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,42 +0,0 @@
|
||||
namespace EnvelopeGenerator.API.Models;
|
||||
|
||||
/// <summary>
|
||||
/// Lightweight payload used by the Blazor receiver UI to submit signatures.
|
||||
/// Each entry corresponds to one signature placeholder rendered as an
|
||||
/// overlay on top of the DevExpress PDF viewer.
|
||||
///
|
||||
/// The legacy MVC flow ships a full PSPDFKit InstantJSON payload via
|
||||
/// <see cref="EnvelopeGenerator.Application.Common.Notifications.DocSigned.PsPdfKitAnnotation"/>.
|
||||
/// That format is browser-library specific. The Blazor client uses a
|
||||
/// transport-neutral DTO instead: only what the server actually needs
|
||||
/// to persist the signed document (image + metadata per placeholder).
|
||||
///
|
||||
/// Conversion to the internal <c>PsPdfKitAnnotation</c> happens inside
|
||||
/// <see cref="EnvelopeGenerator.API.Controllers.AnnotationController"/>.
|
||||
/// </summary>
|
||||
public class BlazorSignaturePayload
|
||||
{
|
||||
/// <summary>The receiver-specific signed placeholders for this envelope.</summary>
|
||||
public List<BlazorSignatureEntry> Signatures { get; set; } = new();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// A single signed placeholder.
|
||||
/// </summary>
|
||||
public class BlazorSignatureEntry
|
||||
{
|
||||
/// <summary>The receiver's <c>DocumentReceiverElement.Id</c> being signed.</summary>
|
||||
public int ElementId { get; set; }
|
||||
|
||||
/// <summary>Signature image as a data URL (e.g. "data:image/png;base64,...").</summary>
|
||||
public string SignatureDataUrl { get; set; } = string.Empty;
|
||||
|
||||
/// <summary>Optional position / job title entered by the receiver.</summary>
|
||||
public string? Position { get; set; }
|
||||
|
||||
/// <summary>Optional city / location entered by the receiver.</summary>
|
||||
public string? City { get; set; }
|
||||
|
||||
/// <summary>Capture timestamp (client local time).</summary>
|
||||
public DateTime SignedAt { get; set; }
|
||||
}
|
||||
@@ -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; }
|
||||
}
|
||||
@@ -1,60 +0,0 @@
|
||||
using AutoMapper;
|
||||
using DigitalData.Core.Abstraction.Application.Repository;
|
||||
using EnvelopeGenerator.Application.Common.Dto.EnvelopeReceiver;
|
||||
using EnvelopeGenerator.Application.Common.Extensions;
|
||||
using EnvelopeGenerator.Application.Common.Query;
|
||||
using EnvelopeGenerator.Application.Envelopes.Queries;
|
||||
using EnvelopeGenerator.Application.Receivers.Queries;
|
||||
using EnvelopeGenerator.Domain.Entities;
|
||||
using MediatR;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
|
||||
namespace EnvelopeGenerator.Application.EnvelopeReceivers.Queries;
|
||||
|
||||
/// <summary>
|
||||
/// Leichte Variante von <see cref="ReadEnvelopeReceiverQuery"/>:
|
||||
/// Lädt NUR Envelope (mit Histories + User) und Receiver.
|
||||
/// Lädt NICHT Documents/Elements.
|
||||
///
|
||||
/// Verwendung: Status-Prüfungen wo keine Dokument-Daten benötigt werden
|
||||
/// (z.B. ReceiverAuthController.GetStatus).
|
||||
/// </summary>
|
||||
public record ReadEnvelopeReceiverLightQuery
|
||||
: EnvelopeReceiverQueryBase<ReadEnvelopeQuery, ReadReceiverQuery>, IRequest<EnvelopeReceiverDto?>;
|
||||
|
||||
/// <summary>
|
||||
/// Handler für <see cref="ReadEnvelopeReceiverLightQuery"/>.
|
||||
/// </summary>
|
||||
public class ReadEnvelopeReceiverLightQueryHandler
|
||||
: IRequestHandler<ReadEnvelopeReceiverLightQuery, EnvelopeReceiverDto?>
|
||||
{
|
||||
private readonly IRepository<EnvelopeReceiver> _repo;
|
||||
private readonly IMapper _mapper;
|
||||
|
||||
/// <summary>
|
||||
/// Konstruktor.
|
||||
/// </summary>
|
||||
/// <param name="repo">EnvelopeReceiver-Repository</param>
|
||||
/// <param name="mapper">AutoMapper</param>
|
||||
public ReadEnvelopeReceiverLightQueryHandler(IRepository<EnvelopeReceiver> repo, IMapper mapper)
|
||||
{
|
||||
_repo = repo;
|
||||
_mapper = mapper;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Lädt einen EnvelopeReceiver ohne Documents/Elements.
|
||||
/// </summary>
|
||||
public async Task<EnvelopeReceiverDto?> Handle(ReadEnvelopeReceiverLightQuery request, CancellationToken cancel)
|
||||
{
|
||||
var envRcv = await _repo.Query
|
||||
.Where(request, notnull: false)
|
||||
.AsNoTracking()
|
||||
.Include(er => er.Envelope).ThenInclude(e => e!.Histories)
|
||||
.Include(er => er.Envelope).ThenInclude(e => e!.User)
|
||||
.Include(er => er.Receiver)
|
||||
.FirstOrDefaultAsync(cancel);
|
||||
|
||||
return envRcv is null ? null : _mapper.Map<EnvelopeReceiverDto>(envRcv);
|
||||
}
|
||||
}
|
||||
10
EnvelopeGenerator.ReceiverUI/App.razor
Normal file
10
EnvelopeGenerator.ReceiverUI/App.razor
Normal file
@@ -0,0 +1,10 @@
|
||||
<Router AppAssembly="@typeof(Program).Assembly">
|
||||
<Found Context="routeData">
|
||||
<RouteView RouteData="@routeData" DefaultLayout="@typeof(MainLayout)" />
|
||||
</Found>
|
||||
<NotFound>
|
||||
<LayoutView Layout="@typeof(MainLayout)">
|
||||
<p>Sorry, there's nothing at this address.</p>
|
||||
</LayoutView>
|
||||
</NotFound>
|
||||
</Router>
|
||||
44
EnvelopeGenerator.ReceiverUI/Data/Adjustment.cs
Normal file
44
EnvelopeGenerator.ReceiverUI/Data/Adjustment.cs
Normal file
@@ -0,0 +1,44 @@
|
||||
namespace EnvelopeGenerator.ReceiverUI.Data {
|
||||
public class Adjustment
|
||||
{
|
||||
public static Adjustment CreateBalanceForward(DateTime dt, int random)
|
||||
{
|
||||
var rnd = new DeterministicRandom(random);
|
||||
Adjustment res = new Adjustment();
|
||||
res.currentDateTime = dt;
|
||||
res.currentDescription = "Balance Forward";
|
||||
res.currentAmount = rnd.Random(10, 300) * 10;
|
||||
return res;
|
||||
}
|
||||
public static Adjustment CreatePayment(DateTime dt, int random)
|
||||
{
|
||||
var rnd = new DeterministicRandom(random);
|
||||
Adjustment res = new Adjustment();
|
||||
res.currentDateTime = dt;
|
||||
res.currentDescription = "Payment";
|
||||
res.currentAmount = -rnd.Random(1, 40) * 10;
|
||||
return res;
|
||||
}
|
||||
public static Adjustment CreateCharge(DateTime dt, int random)
|
||||
{
|
||||
var rnd = new DeterministicRandom(random);
|
||||
Adjustment res = new Adjustment();
|
||||
res.currentDateTime = dt;
|
||||
res.currentDescription = rnd.GetRandomItem(bills);
|
||||
res.currentAmount = rnd.Random(10, 50) * 10;
|
||||
return res;
|
||||
}
|
||||
|
||||
DateTime currentDateTime;
|
||||
string currentDescription = "";
|
||||
double currentAmount = 0;
|
||||
static readonly string[] bills = new string[] { "Bill - Insurance", "Bill - Electricity", "Bill - Rent", "Bill - Phone", "Bill - Office Supplies" };
|
||||
public DateTime Date { get { return currentDateTime; } }
|
||||
public string Description { get { return currentDescription; } }
|
||||
public double Amount { get { return currentAmount; } }
|
||||
|
||||
public Adjustment()
|
||||
{
|
||||
}
|
||||
}
|
||||
}
|
||||
64
EnvelopeGenerator.ReceiverUI/Data/Customer.cs
Normal file
64
EnvelopeGenerator.ReceiverUI/Data/Customer.cs
Normal file
@@ -0,0 +1,64 @@
|
||||
using DevExpress.DataAccess.Sql;
|
||||
using DevExpress.DataAccess.Sql.DataApi;
|
||||
|
||||
namespace EnvelopeGenerator.ReceiverUI.Data {
|
||||
public class Customer {
|
||||
static List<Customer> currentCustomers = new List<Customer>();
|
||||
|
||||
public static List<Customer> Customers { get { return currentCustomers; } }
|
||||
static Customer() {
|
||||
try {
|
||||
SqlDataSource ds = new SqlDataSource("NWindConnectionString");
|
||||
SelectQuery query = SelectQueryFluentBuilder
|
||||
.AddTable("Customers")
|
||||
.SelectAllColumns()
|
||||
.Build("Customers");
|
||||
ds.Queries.Add(query);
|
||||
ds.RebuildResultSchema();
|
||||
ds.Fill();
|
||||
ITable src = ds.Result["Customers"];
|
||||
foreach(var row in src) {
|
||||
currentCustomers.Add(new Customer() {
|
||||
CustomerID = row.GetValue<string>("CustomerID"),
|
||||
Address = row.GetValue<string>("Address"),
|
||||
CompanyName = row.GetValue<string>("CompanyName"),
|
||||
ContactName = row.GetValue<string>("ContactName"),
|
||||
ContactTitle = row.GetValue<string>("ContactTitle"),
|
||||
Country = row.GetValue<string>("Country"),
|
||||
City = row.GetValue<string>("City"),
|
||||
Fax = row.GetValue<string>("Fax"),
|
||||
Phone = row.GetValue<string>("Phone"),
|
||||
PostalCode = row.GetValue<string>("PostalCode"),
|
||||
Region = row.GetValue<string>("Region")
|
||||
});
|
||||
}
|
||||
} catch {
|
||||
currentCustomers.Add(new Customer() {
|
||||
Address = "Obere Str. 57",
|
||||
City = "Berlin",
|
||||
CompanyName = "Alfreds Futterkiste",
|
||||
ContactName = "Maria Anders",
|
||||
ContactTitle = "Sales Representative",
|
||||
Country = "Germany",
|
||||
CustomerID = "ALFKI",
|
||||
Fax = "030-0076545",
|
||||
Phone = "030-0074321",
|
||||
PostalCode = "12209"
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
public string CustomerID { get; set; }
|
||||
public string CompanyName { get; set; }
|
||||
public string ContactName { get; set; }
|
||||
public string ContactTitle { get; set; }
|
||||
public string Address { get; set; }
|
||||
public string City { get; set; }
|
||||
public string PostalCode { get; set; }
|
||||
public string Region { get; set; }
|
||||
public string Country { get; set; }
|
||||
public string Phone { get; set; }
|
||||
public string Fax { get; set; }
|
||||
|
||||
}
|
||||
}
|
||||
71
EnvelopeGenerator.ReceiverUI/Data/DataItem.cs
Normal file
71
EnvelopeGenerator.ReceiverUI/Data/DataItem.cs
Normal file
@@ -0,0 +1,71 @@
|
||||
namespace EnvelopeGenerator.ReceiverUI.Data {
|
||||
public class DataItem {
|
||||
static readonly string[] accountType = new string[] { "Energy", "Manufacturing", "Estate", "Food", "Services" };
|
||||
public string CustomerID { get; set; }
|
||||
public string CompanyName { get; set; }
|
||||
public string ContactName { get; set; }
|
||||
public string ContactTitle { get; set; }
|
||||
public string Address { get; set; }
|
||||
public string City { get; set; }
|
||||
public string PostalCode { get; set; }
|
||||
public string Region { get; set; }
|
||||
public string Country { get; set; }
|
||||
public string Phone { get; set; }
|
||||
public string Fax { get; set; }
|
||||
public string Email { get; set; }
|
||||
public string Invoice { get; set; }
|
||||
public string CustomerAccount { get; set; }
|
||||
public string CustomerIdentifiers { get; set; }
|
||||
public DateTime BillingDate { get; set; }
|
||||
public DateTime BillingPeriodStart { get; set; }
|
||||
public DateTime BillingPeriodEnd { get; set; }
|
||||
public string Terms { get; set; }
|
||||
public string TermsID { get; set; }
|
||||
public Adjustment[] Adjustments { get; set; }
|
||||
|
||||
public DataItem(int i) {
|
||||
var rnd = new DeterministicRandom(i);
|
||||
Customer c = rnd.GetRandomItem(Customer.Customers);
|
||||
CustomerID = c.CustomerID;
|
||||
CompanyName = c.CompanyName;
|
||||
ContactName = c.ContactName;
|
||||
ContactTitle = c.ContactTitle;
|
||||
Address = c.Address;
|
||||
City = c.City;
|
||||
PostalCode = c.PostalCode;
|
||||
Region = c.Region;
|
||||
Country = c.Country;
|
||||
Phone = c.Phone;
|
||||
Fax = c.Fax;
|
||||
Email = ContactName.Split(' ')[0].Replace(' ', '.').ToLower() + "@" + CompanyName.Split(' ')[0].ToLower() + ".com";
|
||||
Invoice = string.Format("{0}{1}-{2}", rnd.RandomChar, rnd.Random(100, 1000), rnd.Random(100, 1000));
|
||||
CustomerAccount = rnd.GetRandomItem(accountType);
|
||||
CustomerIdentifiers = string.Format("{0}-{1}", rnd.Random(1000, 10000), rnd.Random(10, 100));
|
||||
BillingPeriodStart = rnd.RandomTime();
|
||||
BillingPeriodEnd = rnd.RandomTime(BillingPeriodStart, 7 * 24, 30 * 24);
|
||||
BillingDate = rnd.RandomTime(BillingPeriodEnd, 7 * 24, 30 * 24);
|
||||
Term currentTerm = rnd.GetRandomItem(Term.Terms);
|
||||
Terms = currentTerm.Name;
|
||||
|
||||
int adjustmentsCount = rnd.Random(6) + 4;
|
||||
Adjustments = new Adjustment[adjustmentsCount];
|
||||
int h = (int)((BillingPeriodEnd - BillingPeriodStart).TotalHours / adjustmentsCount);
|
||||
|
||||
Adjustments[0] = Adjustment.CreateBalanceForward(rnd.RandomTime(BillingPeriodStart, 0, h), rnd.Random(10000));
|
||||
|
||||
int[] items = rnd.RandomList(adjustmentsCount - 1, 2);
|
||||
|
||||
for(int j = 1; j < Adjustments.Length; j++) {
|
||||
DateTime nextDate = rnd.RandomTime(BillingPeriodStart.AddHours(h * j), 0, h);
|
||||
switch(items[j - 1]) {
|
||||
case 0:
|
||||
Adjustments[j] = Adjustment.CreateCharge(nextDate, rnd.Random(10000));
|
||||
break;
|
||||
case 1:
|
||||
Adjustments[j] = Adjustment.CreatePayment(nextDate, rnd.Random(10000));
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
70
EnvelopeGenerator.ReceiverUI/Data/DataItemList.cs
Normal file
70
EnvelopeGenerator.ReceiverUI/Data/DataItemList.cs
Normal file
@@ -0,0 +1,70 @@
|
||||
using System.Collections;
|
||||
|
||||
namespace EnvelopeGenerator.ReceiverUI.Data {
|
||||
public class DataItemList : IList<DataItem>, IList {
|
||||
readonly int rowCount;
|
||||
|
||||
public DataItem this[int index] { get { return new DataItem(index); } set { } }
|
||||
public int Count { get { return rowCount; } }
|
||||
public bool IsReadOnly { get { return false; } }
|
||||
public bool IsFixedSize { get { return false; } }
|
||||
public object SyncRoot { get { return true; } }
|
||||
public bool IsSynchronized { get { return true; } }
|
||||
object IList.this[int index] { get { return new DataItem(index); } set { } }
|
||||
|
||||
public DataItemList(int rowCount) {
|
||||
this.rowCount = rowCount;
|
||||
}
|
||||
public IEnumerator<DataItem> GetEnumerator() {
|
||||
throw new NotImplementedException();
|
||||
}
|
||||
public int Add(object value) {
|
||||
throw new NotImplementedException();
|
||||
}
|
||||
public bool Contains(object value) {
|
||||
throw new NotImplementedException();
|
||||
}
|
||||
public void Clear() {
|
||||
throw new NotImplementedException();
|
||||
}
|
||||
public int IndexOf(object value) {
|
||||
throw new NotImplementedException();
|
||||
}
|
||||
public void Insert(int index, object value) {
|
||||
throw new NotImplementedException();
|
||||
}
|
||||
public void Remove(object value) {
|
||||
throw new NotImplementedException();
|
||||
}
|
||||
public void RemoveAt(int index) {
|
||||
throw new NotImplementedException();
|
||||
}
|
||||
public void CopyTo(Array array, int index) {
|
||||
throw new NotImplementedException();
|
||||
}
|
||||
IEnumerator IEnumerable.GetEnumerator() {
|
||||
throw new NotImplementedException();
|
||||
}
|
||||
public int IndexOf(DataItem item) {
|
||||
throw new NotImplementedException();
|
||||
}
|
||||
public void Insert(int index, DataItem item) {
|
||||
throw new NotImplementedException();
|
||||
}
|
||||
public void Add(DataItem item) {
|
||||
throw new NotImplementedException();
|
||||
}
|
||||
public bool Contains(DataItem item) {
|
||||
throw new NotImplementedException();
|
||||
}
|
||||
public void CopyTo(DataItem[] array, int arrayIndex) {
|
||||
throw new NotImplementedException();
|
||||
}
|
||||
public bool Remove(DataItem item) {
|
||||
throw new NotImplementedException();
|
||||
}
|
||||
void ICollection<DataItem>.CopyTo(DataItem[] array, int arrayIndex) {
|
||||
CopyTo(array, arrayIndex);
|
||||
}
|
||||
}
|
||||
}
|
||||
60
EnvelopeGenerator.ReceiverUI/Data/DeterministicRandom.cs
Normal file
60
EnvelopeGenerator.ReceiverUI/Data/DeterministicRandom.cs
Normal file
@@ -0,0 +1,60 @@
|
||||
namespace EnvelopeGenerator.ReceiverUI.Data {
|
||||
class DeterministicRandom {
|
||||
const int randomCount = 10000;
|
||||
static readonly int[] deterministicRandomNumbers;
|
||||
static readonly DateTime time;
|
||||
int rnd;
|
||||
int Next {
|
||||
get {
|
||||
rnd = deterministicRandomNumbers[rnd % randomCount];
|
||||
return rnd;
|
||||
}
|
||||
}
|
||||
public char RandomChar {
|
||||
get {
|
||||
return (char)((int)'A' + Random(0, 26));
|
||||
}
|
||||
}
|
||||
public int[] RandomList(int count, int to) {
|
||||
int[] res = new int[count];
|
||||
for(int i = 0; i < Math.Min(count, to); i++)
|
||||
res[i] = i;
|
||||
for(int i = to; i < count; i++)
|
||||
res[i] = Random(to);
|
||||
|
||||
for(int i = 0; i < count; i++) {
|
||||
int ind = Random(count);
|
||||
int temp = res[ind];
|
||||
res[ind] = res[i];
|
||||
res[i] = temp;
|
||||
}
|
||||
return res;
|
||||
}
|
||||
public int Random(int to) {
|
||||
return Random(0, to);
|
||||
}
|
||||
public int Random(int from, int to) {
|
||||
return Next % Math.Max(1, to - from) + from;
|
||||
}
|
||||
public T GetRandomItem<T>(IList<T> list) {
|
||||
return list[Next % list.Count];
|
||||
}
|
||||
public DateTime RandomTime() {
|
||||
return RandomTime(time, 0, 30 * 24);
|
||||
}
|
||||
public DateTime RandomTime(DateTime from, int fromHours, int toHours) {
|
||||
return from.AddHours(Next % (toHours - fromHours) + fromHours);
|
||||
}
|
||||
|
||||
static DeterministicRandom() {
|
||||
time = DateTime.Now.AddDays(-62);
|
||||
Random currentRandom = new Random(randomCount);
|
||||
deterministicRandomNumbers = new int[randomCount];
|
||||
for(int i = 0; i < randomCount; i++)
|
||||
deterministicRandomNumbers[i] = currentRandom.Next(randomCount);
|
||||
}
|
||||
public DeterministicRandom(int i) {
|
||||
this.rnd = i + (i >> 10) + (i >> 20);
|
||||
}
|
||||
}
|
||||
}
|
||||
15
EnvelopeGenerator.ReceiverUI/Data/Term.cs
Normal file
15
EnvelopeGenerator.ReceiverUI/Data/Term.cs
Normal file
@@ -0,0 +1,15 @@
|
||||
namespace EnvelopeGenerator.ReceiverUI.Data {
|
||||
public struct Term {
|
||||
public static readonly Term[] Terms = new Term[] {
|
||||
new Term("Payment seven days after invoice date" ),
|
||||
new Term("Payment ten days after invoice date" ),
|
||||
new Term("End of month" ),
|
||||
new Term("21st of the month following invoice date" ),
|
||||
};
|
||||
readonly string currentName;
|
||||
public string Name { get { return currentName; } }
|
||||
public Term(string currentName) {
|
||||
this.currentName = currentName;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk.BlazorWebAssembly">
|
||||
<PropertyGroup>
|
||||
<TargetFramework>net8.0</TargetFramework>
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
<WasmBuildNative>true</WasmBuildNative>
|
||||
</PropertyGroup>
|
||||
<ItemGroup>
|
||||
<PackageReference Include="HarfBuzzSharp.NativeAssets.WebAssembly" Version="8.3.1.2" />
|
||||
<PackageReference Include="SkiaSharp.NativeAssets.WebAssembly" Version="3.119.1" />
|
||||
<PackageReference Include="SkiaSharp.Views.Blazor" Version="3.119.1" />
|
||||
<NativeFileReference Include="$(HarfBuzzSharpStaticLibraryPath)\2.0.23\*.a" />
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Microsoft.AspNetCore.Components.WebAssembly" Version="8.0.11" />
|
||||
<PackageReference Include="Microsoft.AspNetCore.Components.WebAssembly.DevServer" Version="8.0.11" PrivateAssets="all" />
|
||||
<PackageReference Include="DevExpress.Drawing.Skia" Version="25.2.3" />
|
||||
<PackageReference Include="DevExpress.Blazor.Reporting.JSBasedControls" Version="25.2.3" />
|
||||
<PackageReference Include="DevExpress.Blazor.Reporting.Viewer" Version="25.2.3" />
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<Folder Include="Properties\PublishProfiles\" />
|
||||
</ItemGroup>
|
||||
</Project>
|
||||
12
EnvelopeGenerator.ReceiverUI/NOTICE.txt
Normal file
12
EnvelopeGenerator.ReceiverUI/NOTICE.txt
Normal file
@@ -0,0 +1,12 @@
|
||||
The following open source libraries are used and included within thеse sample/demonstration projects:
|
||||
|
||||
Bootstrap (Open Source – MIT License)
|
||||
Copyright (c) 2011-2021 Twitter, Inc. / Copyright (c) 2011-2021 The Bootstrap Authors
|
||||
https://github.com/twbs/bootstrap/blob/main/LICENSE
|
||||
|
||||
open-iconic (Open Source – MIT License and SIL Open Font License)
|
||||
Copyright (c) 2014 Waybury
|
||||
https://github.com/iconic/open-iconic/blob/master/ICON-LICENSE
|
||||
https://github.com/iconic/open-iconic/blob/master/FONT-LICENSE
|
||||
|
||||
The open source libraries included in these sample/demonstration projects are done so pursuant to each individual open source library license and subject to the disclaimers and limitations on liability set forth in each open source library license.
|
||||
5
EnvelopeGenerator.ReceiverUI/Pages/DocumentViewer.razor
Normal file
5
EnvelopeGenerator.ReceiverUI/Pages/DocumentViewer.razor
Normal file
@@ -0,0 +1,5 @@
|
||||
@page "/documentviewer"
|
||||
|
||||
<DxDocumentViewer ReportName="LargeDatasetReport" CssClass="dx-blazor-reporting-container" Height="@null" Width="@null">
|
||||
<DxDocumentViewerTabPanelSettings Width="340" />
|
||||
</DxDocumentViewer>
|
||||
40
EnvelopeGenerator.ReceiverUI/Pages/Index.razor
Normal file
40
EnvelopeGenerator.ReceiverUI/Pages/Index.razor
Normal file
@@ -0,0 +1,40 @@
|
||||
@page "/"
|
||||
@inject NavigationManager NavigationManager
|
||||
|
||||
<div class="d-flex justify-content-center align-items-center" style="min-height: 70vh;">
|
||||
<div class="card shadow-sm border-0" style="max-width: 560px; width: 100%;">
|
||||
<div class="card-body p-5 text-center">
|
||||
<div class="rounded-circle bg-primary bg-opacity-10 d-inline-flex align-items-center justify-content-center mb-4" style="width: 72px; height: 72px;">
|
||||
<span class="oi oi-document text-primary" aria-hidden="true" style="font-size: 2rem;"></span>
|
||||
</div>
|
||||
<h2 class="mb-3">Empfänger-UI</h2>
|
||||
<p class="text-muted mb-4">
|
||||
Sie werden zur Empfänger-UI weitergeleitet. Bitte warten Sie einen Moment.
|
||||
</p>
|
||||
|
||||
<DxLoadingPanel Visible="true"
|
||||
Text=""
|
||||
IndicatorVisible="false"
|
||||
IndicatorAreaVisible="false"
|
||||
IsContentBlocked="false"
|
||||
ApplyBackgroundShading="false">
|
||||
<div class="d-flex flex-column align-items-center gap-3 py-3">
|
||||
<div class="spinner-border text-primary" role="status" style="width: 2rem; height: 2rem;">
|
||||
<span class="visually-hidden">Wird geladen...</span>
|
||||
</div>
|
||||
<div class="text-muted small">Weiterleitung wird vorbereitet...</div>
|
||||
</div>
|
||||
</DxLoadingPanel>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@code {
|
||||
protected override async Task OnAfterRenderAsync(bool firstRender) {
|
||||
if(!firstRender)
|
||||
return;
|
||||
|
||||
await Task.Delay(1200);
|
||||
NavigationManager.NavigateTo("/reportviewer");
|
||||
}
|
||||
}
|
||||
19
EnvelopeGenerator.ReceiverUI/Pages/ReportDesigner.razor
Normal file
19
EnvelopeGenerator.ReceiverUI/Pages/ReportDesigner.razor
Normal file
@@ -0,0 +1,19 @@
|
||||
@page "/reportdesigner"
|
||||
@using DevExpress.DataAccess.Json;
|
||||
@using EnvelopeGenerator.ReceiverUI.Services;
|
||||
|
||||
<DxReportDesigner ReportName="LargeDatasetReport" CssClass="dx-blazor-reporting-container" Height="@null" Width="@null" AllowMDI="true" DataSources="DataSources">
|
||||
<DxReportDesignerWizardSettings UseFullscreenWizard="true" />
|
||||
</DxReportDesigner>
|
||||
|
||||
@code {
|
||||
Dictionary<string, object> DataSources = new Dictionary<string, object>();
|
||||
protected override async Task OnInitializedAsync() {
|
||||
await base.OnInitializedAsync();
|
||||
var connection = CustomDataSourceWizardJsonDataConnectionStorage.GetDefaultConnection();
|
||||
JsonDataSource jsonDataSource = new JsonDataSource();
|
||||
jsonDataSource.JsonSource = connection.GetJsonSource();
|
||||
await jsonDataSource.FillAsync();
|
||||
DataSources.Add("Northwind", jsonDataSource);
|
||||
}
|
||||
}
|
||||
178
EnvelopeGenerator.ReceiverUI/Pages/ReportViewer.razor
Normal file
178
EnvelopeGenerator.ReceiverUI/Pages/ReportViewer.razor
Normal file
@@ -0,0 +1,178 @@
|
||||
@page "/reportviewer/"
|
||||
@using System.Drawing
|
||||
@using DevExpress.Drawing
|
||||
@using DevExpress.Utils
|
||||
@using DevExpress.XtraPrinting
|
||||
@using DevExpress.XtraPrinting.Drawing
|
||||
@using DevExpress.XtraReports.UI;
|
||||
@using EnvelopeGenerator.ReceiverUI.Services;
|
||||
@inject IJSRuntime JSRuntime
|
||||
@inject InMemoryReportStorageWebExtension ReportStorage
|
||||
|
||||
<link href="_content/DevExpress.Blazor.Themes/blazing-berry.bs5.min.css" rel="stylesheet" />
|
||||
<link href="_content/DevExpress.Blazor.Reporting.Viewer/css/dx-blazor-reporting-components.bs5.css" rel="stylesheet" />
|
||||
|
||||
<div class="card m-3">
|
||||
<div class="card-body">
|
||||
<h5 class="card-title">Unterschrift</h5>
|
||||
<p class="card-text text-muted mb-2">
|
||||
@if(SignatureApplied) {
|
||||
<span>Die Unterschrift wurde dem Bericht hinzugefuegt. Sie koennen die Unterschrift erneuern oder das signierte PDF exportieren.</span>
|
||||
} else {
|
||||
<span>Bitte fuegen Sie vor dem PDF-Export Ihre Unterschrift hinzu.</span>
|
||||
}
|
||||
</p>
|
||||
@if(!string.IsNullOrWhiteSpace(SignatureValidationMessage)) {
|
||||
<div class="text-danger mb-2">@SignatureValidationMessage</div>
|
||||
}
|
||||
<div class="d-flex gap-2 flex-wrap">
|
||||
<button class="btn btn-primary" @onclick="OpenSignaturePopupAsync">
|
||||
@(SignatureApplied ? "Unterschrift erneuern" : "Unterschrift hinzufuegen")
|
||||
</button>
|
||||
<button class="btn btn-success" disabled="@(!SignatureApplied)" @onclick="ExportSignedPdfAsync">Signiertes PDF exportieren</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<DxPopup @bind-Visible="SignaturePopupVisible"
|
||||
HeaderText="Unterschrift erfassen"
|
||||
Width="520px"
|
||||
ShowFooter="true"
|
||||
CloseOnEscape="true"
|
||||
CloseOnOutsideClick="false">
|
||||
<BodyContentTemplate>
|
||||
<p class="text-muted mb-2">Bitte unterschreiben Sie im folgenden Feld.</p>
|
||||
<canvas id="receiver-signature-pad" width="420" height="150" class="border rounded bg-white w-100" style="max-width: 420px; touch-action: none;"></canvas>
|
||||
@if(!string.IsNullOrWhiteSpace(PopupValidationMessage)) {
|
||||
<div class="text-danger mt-2">@PopupValidationMessage</div>
|
||||
}
|
||||
</BodyContentTemplate>
|
||||
<FooterContentTemplate>
|
||||
<div class="d-flex gap-2 flex-wrap justify-content-end w-100">
|
||||
<button class="btn btn-outline-secondary" @onclick="RenewSignatureAsync">Unterschrift erneuern</button>
|
||||
<button class="btn btn-primary" @onclick="ApplySignatureAsync">Zum Bericht hinzufuegen</button>
|
||||
<button class="btn btn-secondary" @onclick="CloseSignaturePopup">Schliessen</button>
|
||||
</div>
|
||||
</FooterContentTemplate>
|
||||
</DxPopup>
|
||||
|
||||
@if(Report is not null) {
|
||||
<DxReportViewer @key="ViewerKey" @ref="reportViewer" Report="Report" RootCssClasses="w-100 h-100" />
|
||||
}
|
||||
|
||||
@code {
|
||||
DxReportViewer reportViewer;
|
||||
XtraReport? Report;
|
||||
bool SignatureApplied;
|
||||
bool SignaturePopupVisible;
|
||||
string? SignatureValidationMessage;
|
||||
string? PopupValidationMessage;
|
||||
int ViewerKey;
|
||||
|
||||
protected override async Task OnInitializedAsync() {
|
||||
Report = CreateReportInstance();
|
||||
|
||||
await Task.CompletedTask;
|
||||
}
|
||||
|
||||
async Task OpenSignaturePopupAsync() {
|
||||
SignaturePopupVisible = true;
|
||||
SignatureValidationMessage = null;
|
||||
PopupValidationMessage = null;
|
||||
await InvokeAsync(StateHasChanged);
|
||||
await Task.Delay(50);
|
||||
await JSRuntime.InvokeVoidAsync("receiverSignature.initialize", "receiver-signature-pad");
|
||||
}
|
||||
|
||||
async Task RenewSignatureAsync() {
|
||||
PopupValidationMessage = null;
|
||||
await JSRuntime.InvokeVoidAsync("receiverSignature.clear", "receiver-signature-pad");
|
||||
}
|
||||
|
||||
void CloseSignaturePopup() {
|
||||
PopupValidationMessage = null;
|
||||
SignaturePopupVisible = false;
|
||||
}
|
||||
|
||||
async Task ApplySignatureAsync() {
|
||||
var signatureDataUrl = await JSRuntime.InvokeAsync<string?>("receiverSignature.getDataUrl", "receiver-signature-pad");
|
||||
|
||||
if(string.IsNullOrWhiteSpace(signatureDataUrl)) {
|
||||
PopupValidationMessage = "Die Unterschrift ist fuer den PDF-Export erforderlich.";
|
||||
return;
|
||||
}
|
||||
|
||||
PopupValidationMessage = null;
|
||||
SignatureValidationMessage = null;
|
||||
Report = CreateSignedReportInstance(signatureDataUrl);
|
||||
SignatureApplied = true;
|
||||
SignaturePopupVisible = false;
|
||||
ViewerKey++;
|
||||
}
|
||||
|
||||
async Task ExportSignedPdfAsync() {
|
||||
if(!SignatureApplied || Report is null) {
|
||||
SignatureValidationMessage = "Bitte fuegen Sie die Unterschrift zuerst zum Bericht hinzu.";
|
||||
return;
|
||||
}
|
||||
|
||||
await reportViewer.ExportToAsync(ExportFormat.Pdf);
|
||||
}
|
||||
|
||||
XtraReport CreateReportInstance() {
|
||||
return ReportStorage.TryGetReport("LargeDatasetReport", out var savedReport)
|
||||
? savedReport
|
||||
: PredefinedReports.ReportsFactory.GetReport("LargeDatasetReport");
|
||||
}
|
||||
|
||||
XtraReport CreateSignedReportInstance(string signatureDataUrl) {
|
||||
var report = CreateReportInstance();
|
||||
AddSignature(report, signatureDataUrl);
|
||||
return report;
|
||||
}
|
||||
|
||||
static void AddSignature(XtraReport report, string signatureDataUrl) {
|
||||
var imageBytes = Convert.FromBase64String(signatureDataUrl[(signatureDataUrl.IndexOf(',') + 1)..]);
|
||||
using var imageStream = new MemoryStream(imageBytes);
|
||||
var imageSource = new ImageSource(DXImage.FromStream(imageStream));
|
||||
var bottomMargin = report.Bands.OfType<BottomMarginBand>().FirstOrDefault();
|
||||
|
||||
if(bottomMargin is null) {
|
||||
bottomMargin = new BottomMarginBand();
|
||||
report.Bands.Add(bottomMargin);
|
||||
}
|
||||
|
||||
bottomMargin.HeightF = Math.Max(bottomMargin.HeightF, 120F);
|
||||
RemoveExistingSignature(bottomMargin);
|
||||
|
||||
var signatureLabel = new XRLabel {
|
||||
Name = "receiverSignatureLabel",
|
||||
Text = $"Empfaengerunterschrift - {DateTime.Now:g}",
|
||||
BoundsF = new RectangleF(390F, 6F, 230F, 18F),
|
||||
Font = new DXFont("Open Sans", 8F, DXFontStyle.Bold),
|
||||
ForeColor = System.Drawing.Color.FromArgb(73, 80, 87),
|
||||
TextAlignment = TextAlignment.MiddleLeft
|
||||
};
|
||||
|
||||
var signature = new XRPictureBox {
|
||||
Name = "receiverSignatureImage",
|
||||
ImageSource = imageSource,
|
||||
BoundsF = new RectangleF(390F, 28F, 230F, 70F),
|
||||
Sizing = ImageSizeMode.ZoomImage,
|
||||
Borders = BorderSide.Bottom,
|
||||
BorderColor = System.Drawing.Color.FromArgb(73, 80, 87)
|
||||
};
|
||||
|
||||
bottomMargin.Controls.AddRange(new XRControl[] { signatureLabel, signature });
|
||||
}
|
||||
|
||||
static void RemoveExistingSignature(BottomMarginBand bottomMargin) {
|
||||
var controls = bottomMargin.Controls
|
||||
.Cast<XRControl>()
|
||||
.Where(control => control.Name is "receiverSignatureLabel" or "receiverSignatureImage")
|
||||
.ToArray();
|
||||
|
||||
foreach(var control in controls)
|
||||
bottomMargin.Controls.Remove(control);
|
||||
}
|
||||
}
|
||||
1198
EnvelopeGenerator.ReceiverUI/PredefinedReports/Report.cs
Normal file
1198
EnvelopeGenerator.ReceiverUI/PredefinedReports/Report.cs
Normal file
File diff suppressed because it is too large
Load Diff
123
EnvelopeGenerator.ReceiverUI/PredefinedReports/Report.resx
Normal file
123
EnvelopeGenerator.ReceiverUI/PredefinedReports/Report.resx
Normal file
@@ -0,0 +1,123 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<root>
|
||||
<!--
|
||||
Microsoft ResX Schema
|
||||
|
||||
Version 2.0
|
||||
|
||||
The primary goals of this format is to allow a simple XML format
|
||||
that is mostly human readable. The generation and parsing of the
|
||||
various data types are done through the TypeConverter classes
|
||||
associated with the data types.
|
||||
|
||||
Example:
|
||||
|
||||
... ado.net/XML headers & schema ...
|
||||
<resheader name="resmimetype">text/microsoft-resx</resheader>
|
||||
<resheader name="version">2.0</resheader>
|
||||
<resheader name="reader">System.Resources.ResXResourceReader, System.Windows.Forms, ...</resheader>
|
||||
<resheader name="writer">System.Resources.ResXResourceWriter, System.Windows.Forms, ...</resheader>
|
||||
<data name="Name1"><value>this is my long string</value><comment>this is a comment</comment></data>
|
||||
<data name="Color1" type="System.Drawing.Color, System.Drawing">Blue</data>
|
||||
<data name="Bitmap1" mimetype="application/x-microsoft.net.object.binary.base64">
|
||||
<value>[base64 mime encoded serialized .NET Framework object]</value>
|
||||
</data>
|
||||
<data name="Icon1" type="System.Drawing.Icon, System.Drawing" mimetype="application/x-microsoft.net.object.bytearray.base64">
|
||||
<value>[base64 mime encoded string representing a byte array form of the .NET Framework object]</value>
|
||||
<comment>This is a comment</comment>
|
||||
</data>
|
||||
|
||||
There are any number of "resheader" rows that contain simple
|
||||
name/value pairs.
|
||||
|
||||
Each data row contains a name, and value. The row also contains a
|
||||
type or mimetype. Type corresponds to a .NET class that support
|
||||
text/value conversion through the TypeConverter architecture.
|
||||
Classes that don't support this are serialized and stored with the
|
||||
mimetype set.
|
||||
|
||||
The mimetype is used for serialized objects, and tells the
|
||||
ResXResourceReader how to depersist the object. This is currently not
|
||||
extensible. For a given mimetype the value must be set accordingly:
|
||||
|
||||
Note - application/x-microsoft.net.object.binary.base64 is the format
|
||||
that the ResXResourceWriter will generate, however the reader can
|
||||
read any of the formats listed below.
|
||||
|
||||
mimetype: application/x-microsoft.net.object.binary.base64
|
||||
value : The object must be serialized with
|
||||
: System.Runtime.Serialization.Formatters.Binary.BinaryFormatter
|
||||
: and then encoded with base64 encoding.
|
||||
|
||||
mimetype: application/x-microsoft.net.object.soap.base64
|
||||
value : The object must be serialized with
|
||||
: System.Runtime.Serialization.Formatters.Soap.SoapFormatter
|
||||
: and then encoded with base64 encoding.
|
||||
|
||||
mimetype: application/x-microsoft.net.object.bytearray.base64
|
||||
value : The object must be serialized into a byte array
|
||||
: using a System.ComponentModel.TypeConverter
|
||||
: and then encoded with base64 encoding.
|
||||
-->
|
||||
<xsd:schema id="root" xmlns="" xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:msdata="urn:schemas-microsoft-com:xml-msdata">
|
||||
<xsd:import namespace="http://www.w3.org/XML/1998/namespace" />
|
||||
<xsd:element name="root" msdata:IsDataSet="true">
|
||||
<xsd:complexType>
|
||||
<xsd:choice maxOccurs="unbounded">
|
||||
<xsd:element name="metadata">
|
||||
<xsd:complexType>
|
||||
<xsd:sequence>
|
||||
<xsd:element name="value" type="xsd:string" minOccurs="0" />
|
||||
</xsd:sequence>
|
||||
<xsd:attribute name="name" use="required" type="xsd:string" />
|
||||
<xsd:attribute name="type" type="xsd:string" />
|
||||
<xsd:attribute name="mimetype" type="xsd:string" />
|
||||
<xsd:attribute ref="xml:space" />
|
||||
</xsd:complexType>
|
||||
</xsd:element>
|
||||
<xsd:element name="assembly">
|
||||
<xsd:complexType>
|
||||
<xsd:attribute name="alias" type="xsd:string" />
|
||||
<xsd:attribute name="name" type="xsd:string" />
|
||||
</xsd:complexType>
|
||||
</xsd:element>
|
||||
<xsd:element name="data">
|
||||
<xsd:complexType>
|
||||
<xsd:sequence>
|
||||
<xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1" />
|
||||
<xsd:element name="comment" type="xsd:string" minOccurs="0" msdata:Ordinal="2" />
|
||||
</xsd:sequence>
|
||||
<xsd:attribute name="name" type="xsd:string" use="required" msdata:Ordinal="1" />
|
||||
<xsd:attribute name="type" type="xsd:string" msdata:Ordinal="3" />
|
||||
<xsd:attribute name="mimetype" type="xsd:string" msdata:Ordinal="4" />
|
||||
<xsd:attribute ref="xml:space" />
|
||||
</xsd:complexType>
|
||||
</xsd:element>
|
||||
<xsd:element name="resheader">
|
||||
<xsd:complexType>
|
||||
<xsd:sequence>
|
||||
<xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1" />
|
||||
</xsd:sequence>
|
||||
<xsd:attribute name="name" type="xsd:string" use="required" />
|
||||
</xsd:complexType>
|
||||
</xsd:element>
|
||||
</xsd:choice>
|
||||
</xsd:complexType>
|
||||
</xsd:element>
|
||||
</xsd:schema>
|
||||
<resheader name="resmimetype">
|
||||
<value>text/microsoft-resx</value>
|
||||
</resheader>
|
||||
<resheader name="version">
|
||||
<value>2.0</value>
|
||||
</resheader>
|
||||
<resheader name="reader">
|
||||
<value>System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
|
||||
</resheader>
|
||||
<resheader name="writer">
|
||||
<value>System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
|
||||
</resheader>
|
||||
<metadata name="objectDataSource1.TrayLocation" type="System.Drawing.Point, System.Drawing, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a">
|
||||
<value>17, 17</value>
|
||||
</metadata>
|
||||
</root>
|
||||
@@ -0,0 +1,14 @@
|
||||
using DevExpress.XtraReports.UI;
|
||||
|
||||
namespace EnvelopeGenerator.ReceiverUI.PredefinedReports {
|
||||
public static class ReportsFactory
|
||||
{
|
||||
public static readonly Dictionary<string, Func<XtraReport>> Reports = new() {
|
||||
["LargeDatasetReport"] = () => new PredefinedReports.Report()
|
||||
};
|
||||
|
||||
public static XtraReport GetReport(string reportName) {
|
||||
return Reports[reportName]();
|
||||
}
|
||||
}
|
||||
}
|
||||
32
EnvelopeGenerator.ReceiverUI/Program.cs
Normal file
32
EnvelopeGenerator.ReceiverUI/Program.cs
Normal file
@@ -0,0 +1,32 @@
|
||||
using Microsoft.AspNetCore.Components.WebAssembly.Hosting;
|
||||
using Microsoft.AspNetCore.Components.Web;
|
||||
using EnvelopeGenerator.ReceiverUI;
|
||||
using DevExpress.DataAccess.Web;
|
||||
using EnvelopeGenerator.ReceiverUI.Services;
|
||||
using DevExpress.XtraReports.Services;
|
||||
using DevExpress.Blazor.Reporting;
|
||||
using DevExpress.XtraReports.Web.Extensions;
|
||||
|
||||
var builder = WebAssemblyHostBuilder.CreateDefault(args);
|
||||
builder.RootComponents.Add<App>("#app");
|
||||
builder.RootComponents.Add<HeadOutlet>("head::after");
|
||||
builder.Services.AddScoped(sp => new HttpClient { BaseAddress = new Uri(builder.HostEnvironment.BaseAddress) });
|
||||
|
||||
builder.Services.AddDevExpressWebAssemblyBlazorReportViewer();
|
||||
|
||||
builder.Services.AddDevExpressBlazorReportingWebAssembly(configure => {
|
||||
configure.UseDevelopmentMode();
|
||||
});
|
||||
builder.Services.AddScoped<IDataSourceWizardJsonConnectionStorage, CustomDataSourceWizardJsonDataConnectionStorage>();
|
||||
builder.Services.AddScoped<IJsonDataConnectionProviderFactory, CustomJsonDataConnectionProviderFactory>();
|
||||
builder.Services.AddScoped<IObjectDataSourceWizardTypeProvider, ObjectDataSourceWizardCustomTypeProvider>();
|
||||
DevExpress.Utils.DeserializationSettings.RegisterTrustedClass(typeof(EnvelopeGenerator.ReceiverUI.Data.DataItemList));
|
||||
DevExpress.Utils.DeserializationSettings.RegisterTrustedClass(typeof(EnvelopeGenerator.ReceiverUI.PredefinedReports.Report));
|
||||
builder.Services.AddSingleton<InMemoryReportStorageWebExtension>();
|
||||
builder.Services.AddSingleton<ReportStorageWebExtension>(sp => sp.GetRequiredService<InMemoryReportStorageWebExtension>());
|
||||
builder.Services.AddScoped<IReportProviderAsync, CustomReportProvider>();
|
||||
ReportStorageWebExtension.RegisterExtensionGlobal(new InMemoryReportStorageWebExtension());
|
||||
|
||||
var host = builder.Build();
|
||||
await FontLoader.LoadFonts(host.Services.GetRequiredService<HttpClient>(), new List<string> { "opensans.ttf" });
|
||||
await host.RunAsync();
|
||||
@@ -0,0 +1,17 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<!-- https://go.microsoft.com/fwlink/?LinkID=208121. -->
|
||||
<Project>
|
||||
<PropertyGroup>
|
||||
<WebPublishMethod>Package</WebPublishMethod>
|
||||
<LastUsedBuildConfiguration>Release</LastUsedBuildConfiguration>
|
||||
<LastUsedPlatform>Any CPU</LastUsedPlatform>
|
||||
<SiteUrlToLaunchAfterPublish />
|
||||
<LaunchSiteAfterPublish>true</LaunchSiteAfterPublish>
|
||||
<ExcludeApp_Data>false</ExcludeApp_Data>
|
||||
<ProjectGuid>fb2d306b-1042-4a70-31ed-f991a1599371</ProjectGuid>
|
||||
<DesktopBuildPackageLocation>M:\App&Service\0 DD - Smart UP\signFLOW\ReceiverUI\net8\$(Version)\EnvelopeGenerator.ReceiverUI.zip</DesktopBuildPackageLocation>
|
||||
<PackageAsSingleFile>true</PackageAsSingleFile>
|
||||
<DeployIisAppPath>EnvelopeGenerator.ReceiverUI</DeployIisAppPath>
|
||||
<_TargetId>IISWebDeployPackage</_TargetId>
|
||||
</PropertyGroup>
|
||||
</Project>
|
||||
12
EnvelopeGenerator.ReceiverUI/Properties/launchSettings.json
Normal file
12
EnvelopeGenerator.ReceiverUI/Properties/launchSettings.json
Normal file
@@ -0,0 +1,12 @@
|
||||
{
|
||||
"profiles": {
|
||||
"EnvelopeGenerator.ReceiverUI": {
|
||||
"commandName": "Project",
|
||||
"launchBrowser": true,
|
||||
"environmentVariables": {
|
||||
"ASPNETCORE_ENVIRONMENT": "Development"
|
||||
},
|
||||
"applicationUrl": "https://localhost:52936;http://localhost:52937"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,40 @@
|
||||
using DevExpress.DataAccess.Json;
|
||||
using DevExpress.DataAccess.Web;
|
||||
using DevExpress.DataAccess.Wizard.Services;
|
||||
|
||||
namespace EnvelopeGenerator.ReceiverUI.Services
|
||||
{
|
||||
public class CustomDataSourceWizardJsonDataConnectionStorage : IDataSourceWizardJsonConnectionStorage
|
||||
{
|
||||
public static JsonDataConnection GetDefaultConnection() {
|
||||
var uriJsonSource = new UriJsonSource() {
|
||||
Uri = new Uri(@"https://raw.githubusercontent.com/DevExpress-Examples/DataSources/master/JSON/customers.json"),
|
||||
};
|
||||
return new JsonDataConnection(uriJsonSource) { StoreConnectionNameOnly = true, Name = "NWindProductsJson" };
|
||||
}
|
||||
public static List<JsonDataConnection> GetConnections() {
|
||||
var connections = new List<JsonDataConnection> {
|
||||
GetDefaultConnection()
|
||||
};
|
||||
return connections;
|
||||
}
|
||||
|
||||
bool IJsonConnectionStorageService.CanSaveConnection => false;
|
||||
bool IJsonConnectionStorageService.ContainsConnection(string connectionName) {
|
||||
return GetConnections().Any(x => x.Name == connectionName);
|
||||
}
|
||||
|
||||
IEnumerable<JsonDataConnection> IJsonConnectionStorageService.GetConnections() {
|
||||
return GetConnections();
|
||||
}
|
||||
|
||||
JsonDataConnection IJsonDataConnectionProviderService.GetJsonDataConnection(string name) {
|
||||
var connection = GetConnections().FirstOrDefault(x => x.Name == name);
|
||||
if(connection == null)
|
||||
throw new InvalidOperationException();
|
||||
return connection;
|
||||
}
|
||||
|
||||
void IJsonConnectionStorageService.SaveConnection(string connectionName, JsonDataConnection connection, bool saveCredentials) { }
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,24 @@
|
||||
using DevExpress.DataAccess.Json;
|
||||
using DevExpress.DataAccess.Web;
|
||||
namespace EnvelopeGenerator.ReceiverUI.Services
|
||||
{
|
||||
public class CustomJsonDataConnectionProviderFactory : IJsonDataConnectionProviderFactory {
|
||||
public IJsonDataConnectionProviderService Create() {
|
||||
return new WebDocumentViewerJsonDataConnectionProvider(CustomDataSourceWizardJsonDataConnectionStorage.GetConnections());
|
||||
}
|
||||
}
|
||||
|
||||
public class WebDocumentViewerJsonDataConnectionProvider : IJsonDataConnectionProviderService
|
||||
{
|
||||
readonly List<JsonDataConnection> jsonDataConnections;
|
||||
public WebDocumentViewerJsonDataConnectionProvider(List<JsonDataConnection> jsonDataConnections) {
|
||||
this.jsonDataConnections = jsonDataConnections;
|
||||
}
|
||||
public JsonDataConnection GetJsonDataConnection(string name) {
|
||||
var connection = jsonDataConnections.FirstOrDefault(x => x.Name == name);
|
||||
if(connection == null)
|
||||
throw new InvalidOperationException();
|
||||
return connection;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,21 @@
|
||||
using DevExpress.XtraReports.UI;
|
||||
using DevExpress.XtraReports.Services;
|
||||
using EnvelopeGenerator.ReceiverUI.PredefinedReports;
|
||||
|
||||
namespace EnvelopeGenerator.ReceiverUI.Services
|
||||
{
|
||||
public class CustomReportProvider : IReportProviderAsync {
|
||||
private readonly InMemoryReportStorageWebExtension reportStorage;
|
||||
|
||||
public CustomReportProvider(InMemoryReportStorageWebExtension reportStorage) {
|
||||
this.reportStorage = reportStorage;
|
||||
}
|
||||
|
||||
public Task<XtraReport> GetReportAsync(string id, ReportProviderContext context) {
|
||||
if(reportStorage.TryGetReport(id, out var savedReport))
|
||||
return Task.FromResult(savedReport);
|
||||
|
||||
return Task.FromResult(ReportsFactory.GetReport(id));
|
||||
}
|
||||
}
|
||||
}
|
||||
12
EnvelopeGenerator.ReceiverUI/Services/FontLoader.cs
Normal file
12
EnvelopeGenerator.ReceiverUI/Services/FontLoader.cs
Normal file
@@ -0,0 +1,12 @@
|
||||
using DevExpress.Drawing;
|
||||
|
||||
namespace EnvelopeGenerator.ReceiverUI.Services {
|
||||
public static class FontLoader {
|
||||
public async static Task LoadFonts(HttpClient httpClient, List<string> fontNames) {
|
||||
foreach(var fontName in fontNames) {
|
||||
var fontBytes = await httpClient.GetByteArrayAsync($"fonts/{fontName}");
|
||||
DXFontRepository.Instance.AddFont(fontBytes);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,83 @@
|
||||
using DevExpress.XtraReports.UI;
|
||||
using DevExpress.XtraReports.Web.Extensions;
|
||||
using EnvelopeGenerator.ReceiverUI.PredefinedReports;
|
||||
|
||||
namespace EnvelopeGenerator.ReceiverUI.Services;
|
||||
|
||||
public class InMemoryReportStorageWebExtension : ReportStorageWebExtension
|
||||
{
|
||||
private const string DefaultReportName = "LargeDatasetReport";
|
||||
private static readonly Dictionary<string, byte[]> Reports = new(StringComparer.OrdinalIgnoreCase);
|
||||
|
||||
public override bool CanSetData(string url) => IsValidUrl(url);
|
||||
|
||||
public override byte[] GetData(string url)
|
||||
{
|
||||
url = NormalizeUrl(url);
|
||||
|
||||
if (Reports.TryGetValue(url, out var reportLayout))
|
||||
return reportLayout;
|
||||
|
||||
if (ReportsFactory.Reports.TryGetValue(url, out var reportFactory))
|
||||
return SaveReport(reportFactory());
|
||||
|
||||
throw new DevExpress.XtraReports.Web.ClientControls.FaultException($"Report '{url}' was not found.");
|
||||
}
|
||||
|
||||
public override Dictionary<string, string> GetUrls()
|
||||
{
|
||||
var urls = ReportsFactory.Reports.Keys
|
||||
.Concat(Reports.Keys)
|
||||
.Distinct(StringComparer.OrdinalIgnoreCase)
|
||||
.ToDictionary(name => name, name => name, StringComparer.OrdinalIgnoreCase);
|
||||
|
||||
return urls;
|
||||
}
|
||||
|
||||
public override bool IsValidUrl(string url)
|
||||
{
|
||||
return !string.IsNullOrWhiteSpace(url)
|
||||
&& url.IndexOfAny(Path.GetInvalidFileNameChars()) < 0;
|
||||
}
|
||||
|
||||
public override void SetData(XtraReport report, string url)
|
||||
{
|
||||
url = NormalizeUrl(url);
|
||||
Reports[url] = SaveReport(report);
|
||||
}
|
||||
|
||||
public override string SetNewData(XtraReport report, string defaultUrl)
|
||||
{
|
||||
var url = NormalizeUrl(defaultUrl);
|
||||
Reports[url] = SaveReport(report);
|
||||
return url;
|
||||
}
|
||||
|
||||
public bool TryGetReport(string url, out XtraReport report)
|
||||
{
|
||||
url = NormalizeUrl(url);
|
||||
|
||||
if (!Reports.ContainsKey(url))
|
||||
{
|
||||
report = null!;
|
||||
return false;
|
||||
}
|
||||
|
||||
using var stream = new MemoryStream(Reports[url]);
|
||||
report = XtraReport.FromXmlStream(stream, true);
|
||||
report.Name = url;
|
||||
return true;
|
||||
}
|
||||
|
||||
private static string NormalizeUrl(string url)
|
||||
{
|
||||
return string.IsNullOrWhiteSpace(url) ? DefaultReportName : url;
|
||||
}
|
||||
|
||||
private static byte[] SaveReport(XtraReport report)
|
||||
{
|
||||
using var stream = new MemoryStream();
|
||||
report.SaveLayoutToXml(stream);
|
||||
return stream.ToArray();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
using DevExpress.DataAccess.Web;
|
||||
|
||||
namespace EnvelopeGenerator.ReceiverUI.Services {
|
||||
public class ObjectDataSourceWizardCustomTypeProvider : IObjectDataSourceWizardTypeProvider {
|
||||
public IEnumerable<Type> GetAvailableTypes(string context) {
|
||||
return new[] { typeof(Data.DataItemList) };
|
||||
}
|
||||
}
|
||||
}
|
||||
21
EnvelopeGenerator.ReceiverUI/Shared/Header.razor
Normal file
21
EnvelopeGenerator.ReceiverUI/Shared/Header.razor
Normal file
@@ -0,0 +1,21 @@
|
||||
<nav class="navbar header-navbar p-0">
|
||||
<button class="navbar-toggler bg-primary d-block" @onclick="OnToggleClick">
|
||||
<span class="navbar-toggler-icon"></span>
|
||||
</button>
|
||||
<div class="ms-3 fw-bold title pe-4">EnvelopeGenerator.ReceiverUI</div>
|
||||
</nav>
|
||||
|
||||
@code {
|
||||
[Parameter] public bool ToggleOn { get; set; }
|
||||
[Parameter] public EventCallback<bool> ToggleOnChanged { get; set; }
|
||||
|
||||
async Task OnToggleClick() => await Toggle();
|
||||
|
||||
async Task Toggle(bool? value = null) {
|
||||
var newValue = value ?? !ToggleOn;
|
||||
if(ToggleOn != newValue) {
|
||||
ToggleOn = newValue;
|
||||
await ToggleOnChanged.InvokeAsync(ToggleOn);
|
||||
}
|
||||
}
|
||||
}
|
||||
39
EnvelopeGenerator.ReceiverUI/Shared/Header.razor.css
Normal file
39
EnvelopeGenerator.ReceiverUI/Shared/Header.razor.css
Normal file
@@ -0,0 +1,39 @@
|
||||
.navbar.header-navbar {
|
||||
flex-grow: 0;
|
||||
flex-wrap: nowrap;
|
||||
border: none;
|
||||
background-color: inherit;
|
||||
border-radius: 0;
|
||||
height: 3.5rem;
|
||||
min-height: 3.5rem;
|
||||
box-shadow: 0px 2px 6px 0px rgba(0, 0, 0, 0.12);
|
||||
justify-content: flex-start;
|
||||
}
|
||||
|
||||
.header-navbar .navbar-toggler {
|
||||
outline: none;
|
||||
border-radius: 0;
|
||||
padding-left: .75rem;
|
||||
padding-right: .75rem;
|
||||
box-shadow: none;
|
||||
align-self: stretch;
|
||||
}
|
||||
|
||||
.header-navbar .navbar-toggler .navbar-toggler-icon {
|
||||
background-image: url("data:image/svg+xml;charset=utf8,%3Csvg viewBox='0 0 32 32' xmlns='http://www.w3.org/2000/svg'%3E%3Cpath stroke='rgba(255,255,255, 1)' stroke-width='2' stroke-linecap='round' stroke-miterlimit='10' d='M4 8h24M4 16h24M4 24h24'/%3E%3C/svg%3E");
|
||||
background-color: transparent !important;
|
||||
height: 2rem;
|
||||
width: 2rem;
|
||||
}
|
||||
|
||||
.title {
|
||||
font-size: 1.1rem;
|
||||
text-overflow: ellipsis;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
@media (max-width: 350px) {
|
||||
.title {
|
||||
font-size: inherit;
|
||||
}
|
||||
}
|
||||
30
EnvelopeGenerator.ReceiverUI/Shared/MainLayout.razor
Normal file
30
EnvelopeGenerator.ReceiverUI/Shared/MainLayout.razor
Normal file
@@ -0,0 +1,30 @@
|
||||
@using EnvelopeGenerator.ReceiverUI.Services;
|
||||
@inherits LayoutComponentBase
|
||||
|
||||
<div class="page">
|
||||
<div class="sidebar">
|
||||
<NavMenu />
|
||||
</div>
|
||||
|
||||
<main>
|
||||
<div class="top-row px-4">
|
||||
<a href="https://docs.microsoft.com/aspnet/" target="_blank">About</a>
|
||||
</div>
|
||||
|
||||
<article class="content px-4">
|
||||
@Body
|
||||
</article>
|
||||
</main>
|
||||
</div>
|
||||
|
||||
@code {
|
||||
[Inject] HttpClient Http { get; set; }
|
||||
List<string> RequiredFonts = new() {
|
||||
"opensans.ttf"
|
||||
};
|
||||
|
||||
protected async override Task OnInitializedAsync() {
|
||||
await FontLoader.LoadFonts(Http, RequiredFonts);
|
||||
await base.OnInitializedAsync();
|
||||
}
|
||||
}
|
||||
81
EnvelopeGenerator.ReceiverUI/Shared/MainLayout.razor.css
Normal file
81
EnvelopeGenerator.ReceiverUI/Shared/MainLayout.razor.css
Normal file
@@ -0,0 +1,81 @@
|
||||
.page {
|
||||
position: relative;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
overflow: auto;
|
||||
}
|
||||
|
||||
main {
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.sidebar {
|
||||
background-image: linear-gradient(180deg, rgb(5, 39, 103) 0%, #3a0647 70%);
|
||||
}
|
||||
|
||||
.top-row {
|
||||
background-color: #f7f7f7;
|
||||
border-bottom: 1px solid #d6d5d5;
|
||||
justify-content: flex-end;
|
||||
height: 3.5rem;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.top-row ::deep a, .top-row ::deep .btn-link {
|
||||
white-space: nowrap;
|
||||
margin-left: 1.5rem;
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
.top-row ::deep a:hover, .top-row ::deep .btn-link:hover {
|
||||
text-decoration: underline;
|
||||
}
|
||||
|
||||
.top-row ::deep a:first-child {
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
|
||||
@media (max-width: 640.98px) {
|
||||
.top-row:not(.auth) {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.top-row.auth {
|
||||
justify-content: space-between;
|
||||
}
|
||||
|
||||
.top-row ::deep a, .top-row ::deep .btn-link {
|
||||
margin-left: 0;
|
||||
}
|
||||
}
|
||||
|
||||
@media (min-width: 641px) {
|
||||
.page {
|
||||
flex-direction: row;
|
||||
}
|
||||
|
||||
.sidebar {
|
||||
width: 300px;
|
||||
height: 100vh;
|
||||
position: sticky;
|
||||
top: 0;
|
||||
}
|
||||
|
||||
.top-row {
|
||||
position: sticky;
|
||||
top: 0;
|
||||
z-index: 1;
|
||||
}
|
||||
|
||||
.top-row.auth ::deep a:first-child {
|
||||
flex: 1;
|
||||
text-align: right;
|
||||
width: 0;
|
||||
}
|
||||
|
||||
.top-row, article {
|
||||
padding: 2rem !important;
|
||||
}
|
||||
}
|
||||
46
EnvelopeGenerator.ReceiverUI/Shared/NavMenu.razor
Normal file
46
EnvelopeGenerator.ReceiverUI/Shared/NavMenu.razor
Normal file
@@ -0,0 +1,46 @@
|
||||
<div class="top-row ps-3 navbar navbar-dark">
|
||||
<div class="container-fluid">
|
||||
<a class="navbar-brand" href="">EnvelopeGenerator.ReceiverUI</a>
|
||||
<button title="Navigation menu" class="navbar-toggler" @onclick="ToggleNavMenu">
|
||||
<span class="navbar-toggler-icon"></span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="@NavMenuCssClass" @onclick="ToggleNavMenu">
|
||||
<nav class="flex-column">
|
||||
@*
|
||||
<div class="nav-item px-3">
|
||||
<NavLink class="nav-link" href="" Match="NavLinkMatch.All">
|
||||
<span class="oi oi-home" aria-hidden="true"></span> Home
|
||||
</NavLink>
|
||||
</div>
|
||||
<div class="nav-item px-3">
|
||||
<NavLink class="nav-link" href="documentviewer">
|
||||
<span class="oi oi-plus" aria-hidden="true"></span> Document Viewer (JS-Based)
|
||||
</NavLink>
|
||||
</div>
|
||||
*@
|
||||
<div class="nav-item px-3">
|
||||
<NavLink class="nav-link" href="reportviewer">
|
||||
<span class="oi oi-plus" aria-hidden="true"></span> Empfänger-UI
|
||||
</NavLink>
|
||||
</div>
|
||||
<div class="nav-item px-3">
|
||||
<NavLink class="nav-link" href="reportdesigner">
|
||||
<span class="oi oi-plus" aria-hidden="true"></span> Umschlag-UI
|
||||
</NavLink>
|
||||
</div>
|
||||
</nav>
|
||||
</div>
|
||||
|
||||
@code {
|
||||
private bool collapseNavMenu = true;
|
||||
|
||||
private string NavMenuCssClass => collapseNavMenu ? "collapse" : null;
|
||||
|
||||
private void ToggleNavMenu()
|
||||
{
|
||||
collapseNavMenu = !collapseNavMenu;
|
||||
}
|
||||
}
|
||||
63
EnvelopeGenerator.ReceiverUI/Shared/NavMenu.razor.css
Normal file
63
EnvelopeGenerator.ReceiverUI/Shared/NavMenu.razor.css
Normal file
@@ -0,0 +1,63 @@
|
||||
.navbar-toggler {
|
||||
background-color: rgba(255, 255, 255, 0.1);
|
||||
}
|
||||
|
||||
.top-row {
|
||||
height: 3.5rem;
|
||||
background-color: rgba(0,0,0,0.4);
|
||||
}
|
||||
|
||||
.navbar-brand {
|
||||
font-size: 1.1rem;
|
||||
}
|
||||
|
||||
.oi {
|
||||
width: 2rem;
|
||||
font-size: 1.1rem;
|
||||
vertical-align: text-top;
|
||||
top: -2px;
|
||||
}
|
||||
|
||||
.nav-item {
|
||||
font-size: 0.9rem;
|
||||
padding-bottom: 0.5rem;
|
||||
}
|
||||
|
||||
.nav-item:first-of-type {
|
||||
padding-top: 1rem;
|
||||
}
|
||||
|
||||
.nav-item:last-of-type {
|
||||
padding-bottom: 1rem;
|
||||
}
|
||||
|
||||
.nav-item ::deep a {
|
||||
color: #d7d7d7;
|
||||
border-radius: 4px;
|
||||
height: 3rem;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
line-height: 3rem;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.nav-item ::deep a.active {
|
||||
background-color: rgba(255,255,255,0.25);
|
||||
color: white;
|
||||
}
|
||||
|
||||
.nav-item ::deep a:hover {
|
||||
background-color: rgba(255,255,255,0.1);
|
||||
color: white;
|
||||
}
|
||||
|
||||
@media (min-width: 641px) {
|
||||
.navbar-toggler {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.sidebar .collapse {
|
||||
/* Never collapse the sidebar for wide screens */
|
||||
display: block;
|
||||
}
|
||||
}
|
||||
9
EnvelopeGenerator.ReceiverUI/_Imports.razor
Normal file
9
EnvelopeGenerator.ReceiverUI/_Imports.razor
Normal file
@@ -0,0 +1,9 @@
|
||||
@using System.Net.Http
|
||||
@using Microsoft.AspNetCore.Components.Forms
|
||||
@using Microsoft.AspNetCore.Components.Routing
|
||||
@using Microsoft.AspNetCore.Components.Web
|
||||
@using Microsoft.JSInterop
|
||||
@using EnvelopeGenerator.ReceiverUI
|
||||
@using EnvelopeGenerator.ReceiverUI.Shared
|
||||
@using DevExpress.Blazor.Reporting
|
||||
@using DevExpress.Blazor
|
||||
10
EnvelopeGenerator.ReceiverUI/appsettings.Development.json
Normal file
10
EnvelopeGenerator.ReceiverUI/appsettings.Development.json
Normal file
@@ -0,0 +1,10 @@
|
||||
{
|
||||
"Logging": {
|
||||
"IncludeScopes": false,
|
||||
"LogLevel": {
|
||||
"Default": "Information",
|
||||
"System": "Information",
|
||||
"Microsoft": "Information"
|
||||
}
|
||||
}
|
||||
}
|
||||
10
EnvelopeGenerator.ReceiverUI/appsettings.json
Normal file
10
EnvelopeGenerator.ReceiverUI/appsettings.json
Normal file
@@ -0,0 +1,10 @@
|
||||
{
|
||||
"Logging": {
|
||||
"LogLevel": {
|
||||
"Default": "Information",
|
||||
"Microsoft": "Warning",
|
||||
"Microsoft.Hosting.Lifetime": "Information",
|
||||
"DevExpress": "Warning"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,18 @@
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<link href="_content/DevExpress.Blazor.Themes/blazing-berry.bs5.min.css" rel="stylesheet" />
|
||||
|
||||
</head>
|
||||
<body>
|
||||
<div class="d-flex flex-column justify-content-center align-items-center vh-100">
|
||||
<div class="d-flex">
|
||||
<img class="mt-2 me-4" src="images/sad.svg" width="60" height="60" />
|
||||
<div>
|
||||
<div class="h1">Your browser is not supported.</div>
|
||||
<p style="font-size: 1rem; opacity: 0.75;" class="m-0">In .NET 5.0, Blazor does not support Microsoft Internet Explorer and Microsoft Edge Legacy (refer to <a target="_blank" href="https://docs.devexpress.com/Blazor/401588/common-concepts/supported-browsers">Supported Browsers</a>).<br />Please use a different browser to run EnvelopeGenerator.ReceiverUI.</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
51
EnvelopeGenerator.ReceiverUI/wwwroot/css/app.css
Normal file
51
EnvelopeGenerator.ReceiverUI/wwwroot/css/app.css
Normal file
@@ -0,0 +1,51 @@
|
||||
@import url('open-iconic/font/css/open-iconic-bootstrap.min.css');
|
||||
|
||||
html, body {
|
||||
font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
html, body {
|
||||
height: 100%;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
article {
|
||||
height: calc(100vh - 70px);
|
||||
}
|
||||
|
||||
.valid.modified:not([type=checkbox]) {
|
||||
outline: 1px solid #26b050;
|
||||
}
|
||||
|
||||
.invalid {
|
||||
outline: 1px solid red;
|
||||
}
|
||||
|
||||
.validation-message {
|
||||
color: red;
|
||||
}
|
||||
|
||||
#blazor-error-ui {
|
||||
background: lightyellow;
|
||||
bottom: 0;
|
||||
box-shadow: 0 -1px 2px rgba(0, 0, 0, 0.2);
|
||||
display: none;
|
||||
left: 0;
|
||||
padding: 0.6rem 1.25rem 0.7rem 1.25rem;
|
||||
position: fixed;
|
||||
width: 100%;
|
||||
z-index: 1000;
|
||||
}
|
||||
|
||||
#blazor-error-ui .dismiss {
|
||||
cursor: pointer;
|
||||
position: absolute;
|
||||
right: 0.75rem;
|
||||
top: 0.5rem;
|
||||
}
|
||||
|
||||
.dx-blazor-reporting-container {
|
||||
height: calc(100vh - 130px) !important;
|
||||
width: 100% !important;
|
||||
}
|
||||
12063
EnvelopeGenerator.ReceiverUI/wwwroot/css/bootstrap/bootstrap.css
vendored
Normal file
12063
EnvelopeGenerator.ReceiverUI/wwwroot/css/bootstrap/bootstrap.css
vendored
Normal file
File diff suppressed because it is too large
Load Diff
6
EnvelopeGenerator.ReceiverUI/wwwroot/css/bootstrap/bootstrap.min.css
vendored
Normal file
6
EnvelopeGenerator.ReceiverUI/wwwroot/css/bootstrap/bootstrap.min.css
vendored
Normal file
File diff suppressed because one or more lines are too long
@@ -18,4 +18,4 @@ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
||||
THE SOFTWARE.
|
||||
THE SOFTWARE.
|
||||
@@ -1,7 +1,7 @@
|
||||
[Open Iconic v1.1.1](http://useiconic.com/open)
|
||||
[Open Iconic v1.1.1](https://github.com/iconic/open-iconic)
|
||||
===========
|
||||
|
||||
### Open Iconic is the open source sibling of [Iconic](http://useiconic.com). It is a hyper-legible collection of 223 icons with a tiny footprint—ready to use with Bootstrap and Foundation. [View the collection](http://useiconic.com/open#icons)
|
||||
### Open Iconic is the open source sibling of [Iconic](https://github.com/iconic/open-iconic). It is a hyper-legible collection of 223 icons with a tiny footprint—ready to use with Bootstrap and Foundation. [View the collection](https://github.com/iconic/open-iconic)
|
||||
|
||||
|
||||
|
||||
@@ -17,7 +17,7 @@
|
||||
|
||||
## Getting Started
|
||||
|
||||
#### For code samples and everything else you need to get started with Open Iconic, check out our [Icons](http://useiconic.com/open#icons) and [Reference](http://useiconic.com/open#reference) sections.
|
||||
#### For code samples and everything else you need to get started with Open Iconic, check out our [Icons](https://github.com/iconic/open-iconic) and [Reference](https://github.com/iconic/open-iconic) sections.
|
||||
|
||||
### General Usage
|
||||
|
||||
|
Before Width: | Height: | Size: 54 KiB After Width: | Height: | Size: 54 KiB |
BIN
EnvelopeGenerator.ReceiverUI/wwwroot/fonts/opensans.ttf
Normal file
BIN
EnvelopeGenerator.ReceiverUI/wwwroot/fonts/opensans.ttf
Normal file
Binary file not shown.
BIN
EnvelopeGenerator.ReceiverUI/wwwroot/images/banner.png
Normal file
BIN
EnvelopeGenerator.ReceiverUI/wwwroot/images/banner.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 117 KiB |
12
EnvelopeGenerator.ReceiverUI/wwwroot/images/sad.svg
Normal file
12
EnvelopeGenerator.ReceiverUI/wwwroot/images/sad.svg
Normal file
@@ -0,0 +1,12 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<!-- Generator: Adobe Illustrator 24.1.1, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
|
||||
<svg version="1.1" id="_x31_" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
|
||||
viewBox="0 0 62 62" style="enable-background:new 0 0 62 62;" xml:space="preserve">
|
||||
<style type="text/css">
|
||||
.st0{fill-rule:evenodd;clip-rule:evenodd;}
|
||||
</style>
|
||||
<path id="_x32_" class="st0" d="M31,61C14.4,61,1,47.6,1,31S14.4,1,31,1s30,13.4,30,30S47.6,61,31,61z M31,5C16.6,5,5,16.6,5,31
|
||||
s11.6,26,26,26s26-11.6,26-26S45.4,5,31,5z M52,34.5L49.5,37L46,33.5L42.5,37L40,34.5l3.5-3.5L40,27.5l2.5-2.5l3.5,3.5l3.5-3.5
|
||||
l2.5,2.5L48.5,31L52,34.5z M43,44c0,1.1-0.9,2-2,2H21c-1.1,0-2-0.9-2-2s0.9-2,2-2h20C42.1,42,43,42.9,43,44z M19.5,37L16,33.5
|
||||
L12.5,37L10,34.5l3.5-3.5L10,27.5l2.5-2.5l3.5,3.5l3.5-3.5l2.5,2.5L18.5,31l3.5,3.5L19.5,37z"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 893 B |
69
EnvelopeGenerator.ReceiverUI/wwwroot/index.html
Normal file
69
EnvelopeGenerator.ReceiverUI/wwwroot/index.html
Normal file
@@ -0,0 +1,69 @@
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no" />
|
||||
<title>EnvelopeGenerator.ReceiverUI</title>
|
||||
<base href="/" />
|
||||
<link href="css/bootstrap/bootstrap.min.css" rel="stylesheet" />
|
||||
<link href="EnvelopeGenerator.ReceiverUI.styles.css" rel="stylesheet" />
|
||||
<link href="css/app.css" rel="stylesheet" />
|
||||
<style type="text/css">
|
||||
.splash-screen {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
flex-flow: column nowrap;
|
||||
height: 100vh;
|
||||
font-family: "Segoe UI",Roboto,"Helvetica Neue","-apple-system",BlinkMacSystemFont,Arial,sans-serif,"Apple Color Emoji","Segoe UI Emoji","Segoe UI Symbol";
|
||||
font-size: .88rem;
|
||||
font-weight: 400;
|
||||
line-height: 1.5;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.splash-screen .spinner-border {
|
||||
border: .2em solid;
|
||||
border-color: #5f368d #bfbfbf #bfbfbf;
|
||||
width: 120px;
|
||||
height: 120px;
|
||||
}
|
||||
|
||||
.splash-screen-caption {
|
||||
font-size: 1.5rem;
|
||||
font-weight: 600;
|
||||
margin-top: 1.5rem;
|
||||
}
|
||||
|
||||
.splash-screen-text {
|
||||
color: #a1a1a1;
|
||||
margin-top: .5rem;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<div id="app">
|
||||
<script type="text/javascript">
|
||||
if(/MSIE \d|Trident.*rv:|Edge\//.test(window.navigator.userAgent))
|
||||
window.location.href = "browserNotSupported.html";
|
||||
</script>
|
||||
<div class="splash-screen">
|
||||
<div class="spinner-border"></div>
|
||||
<div class="splash-screen-caption">EnvelopeGenerator.ReceiverUI</div>
|
||||
<div class="splash-screen-text">Loading...</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div id="blazor-error-ui">
|
||||
An unhandled error has occurred.
|
||||
<a href="" class="reload">Reload</a>
|
||||
<a class="dismiss">X</a>
|
||||
</div>
|
||||
<script src="_content/DevExpress.Blazor.Resources/js/preload-script.js"></script>
|
||||
<script src="js/receiver-signature.js"></script>
|
||||
<script src="_framework/blazor.webassembly.js"></script>
|
||||
</body>
|
||||
|
||||
</html>
|
||||
@@ -0,0 +1,86 @@
|
||||
window.receiverSignature = (() => {
|
||||
const pads = new Map();
|
||||
|
||||
function getPosition(canvas, event) {
|
||||
const rect = canvas.getBoundingClientRect();
|
||||
const source = event.touches && event.touches.length ? event.touches[0] : event;
|
||||
return {
|
||||
x: (source.clientX - rect.left) * (canvas.width / rect.width),
|
||||
y: (source.clientY - rect.top) * (canvas.height / rect.height)
|
||||
};
|
||||
}
|
||||
|
||||
function initialize(canvasId) {
|
||||
const canvas = document.getElementById(canvasId);
|
||||
if (!canvas || pads.has(canvasId))
|
||||
return;
|
||||
|
||||
const context = canvas.getContext('2d');
|
||||
context.lineWidth = 2.5;
|
||||
context.lineCap = 'round';
|
||||
context.lineJoin = 'round';
|
||||
context.strokeStyle = '#111';
|
||||
|
||||
const state = { drawing: false, hasSignature: false };
|
||||
pads.set(canvasId, state);
|
||||
|
||||
const start = event => {
|
||||
event.preventDefault();
|
||||
const pos = getPosition(canvas, event);
|
||||
state.drawing = true;
|
||||
context.beginPath();
|
||||
context.moveTo(pos.x, pos.y);
|
||||
};
|
||||
|
||||
const move = event => {
|
||||
if (!state.drawing)
|
||||
return;
|
||||
|
||||
event.preventDefault();
|
||||
const pos = getPosition(canvas, event);
|
||||
context.lineTo(pos.x, pos.y);
|
||||
context.stroke();
|
||||
state.hasSignature = true;
|
||||
};
|
||||
|
||||
const end = event => {
|
||||
if (!state.drawing)
|
||||
return;
|
||||
|
||||
event.preventDefault();
|
||||
state.drawing = false;
|
||||
};
|
||||
|
||||
canvas.addEventListener('mousedown', start);
|
||||
canvas.addEventListener('mousemove', move);
|
||||
window.addEventListener('mouseup', end);
|
||||
canvas.addEventListener('touchstart', start, { passive: false });
|
||||
canvas.addEventListener('touchmove', move, { passive: false });
|
||||
canvas.addEventListener('touchend', end, { passive: false });
|
||||
}
|
||||
|
||||
function clear(canvasId) {
|
||||
const canvas = document.getElementById(canvasId);
|
||||
const state = pads.get(canvasId);
|
||||
if (!canvas || !state)
|
||||
return;
|
||||
|
||||
canvas.getContext('2d').clearRect(0, 0, canvas.width, canvas.height);
|
||||
state.hasSignature = false;
|
||||
}
|
||||
|
||||
function getDataUrl(canvasId) {
|
||||
const canvas = document.getElementById(canvasId);
|
||||
const state = pads.get(canvasId);
|
||||
if (!canvas || !state || !state.hasSignature)
|
||||
return null;
|
||||
|
||||
return canvas.toDataURL('image/png');
|
||||
}
|
||||
|
||||
return {
|
||||
initialize,
|
||||
clear,
|
||||
getDataUrl
|
||||
};
|
||||
})();
|
||||
@@ -35,11 +35,7 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "EnvelopeGenerator.Tests", "
|
||||
EndProject
|
||||
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "EnvelopeGenerator.API", "EnvelopeGenerator.API\EnvelopeGenerator.API.csproj", "{EC768913-6270-14F4-1DD3-69C87A659462}"
|
||||
EndProject
|
||||
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "receiverUI", "receiverUI", "{73D8F466-90AA-4F95-9BD1-7CDBB8565162}"
|
||||
EndProject
|
||||
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "EnvelopeGenerator.ReceiverUI.Web", "receiverUI\EnvelopeGenerator.ReceiverUI.Web\EnvelopeGenerator.ReceiverUI.Web\EnvelopeGenerator.ReceiverUI.Web.csproj", "{AAD4720E-D175-44B5-B431-DB0BA636CD20}"
|
||||
EndProject
|
||||
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "EnvelopeGenerator.ReceiverUI.Web.Client", "receiverUI\EnvelopeGenerator.ReceiverUI.Web\EnvelopeGenerator.ReceiverUI.Web.Client\EnvelopeGenerator.ReceiverUI.Web.Client.csproj", "{5D97B2C2-E19B-4958-81E1-38864EC88FEB}"
|
||||
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "EnvelopeGenerator.ReceiverUI", "EnvelopeGenerator.ReceiverUI\EnvelopeGenerator.ReceiverUI.csproj", "{FB2D306B-1042-4A70-31ED-F991A1599371}"
|
||||
EndProject
|
||||
Global
|
||||
GlobalSection(SolutionConfigurationPlatforms) = preSolution
|
||||
@@ -91,14 +87,10 @@ Global
|
||||
{EC768913-6270-14F4-1DD3-69C87A659462}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||
{EC768913-6270-14F4-1DD3-69C87A659462}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||
{EC768913-6270-14F4-1DD3-69C87A659462}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||
{AAD4720E-D175-44B5-B431-DB0BA636CD20}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||
{AAD4720E-D175-44B5-B431-DB0BA636CD20}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||
{AAD4720E-D175-44B5-B431-DB0BA636CD20}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||
{AAD4720E-D175-44B5-B431-DB0BA636CD20}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||
{5D97B2C2-E19B-4958-81E1-38864EC88FEB}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||
{5D97B2C2-E19B-4958-81E1-38864EC88FEB}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||
{5D97B2C2-E19B-4958-81E1-38864EC88FEB}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||
{5D97B2C2-E19B-4958-81E1-38864EC88FEB}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||
{FB2D306B-1042-4A70-31ED-F991A1599371}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||
{FB2D306B-1042-4A70-31ED-F991A1599371}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||
{FB2D306B-1042-4A70-31ED-F991A1599371}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||
{FB2D306B-1042-4A70-31ED-F991A1599371}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||
EndGlobalSection
|
||||
GlobalSection(SolutionProperties) = preSolution
|
||||
HideSolutionNode = FALSE
|
||||
@@ -118,9 +110,7 @@ Global
|
||||
{211619F5-AE25-4BA5-A552-BACAFE0632D3} = {9943209E-1744-4944-B1BA-4F87FC1A0EEB}
|
||||
{224C4845-1CDE-22B7-F3A9-1FF9297F70E8} = {0CBC2432-A561-4440-89BC-671B66A24146}
|
||||
{EC768913-6270-14F4-1DD3-69C87A659462} = {E3C758DC-914D-4B7E-8457-0813F1FDB0CB}
|
||||
{73D8F466-90AA-4F95-9BD1-7CDBB8565162} = {E3C758DC-914D-4B7E-8457-0813F1FDB0CB}
|
||||
{AAD4720E-D175-44B5-B431-DB0BA636CD20} = {73D8F466-90AA-4F95-9BD1-7CDBB8565162}
|
||||
{5D97B2C2-E19B-4958-81E1-38864EC88FEB} = {73D8F466-90AA-4F95-9BD1-7CDBB8565162}
|
||||
{FB2D306B-1042-4A70-31ED-F991A1599371} = {E3C758DC-914D-4B7E-8457-0813F1FDB0CB}
|
||||
EndGlobalSection
|
||||
GlobalSection(ExtensibilityGlobals) = postSolution
|
||||
SolutionGuid = {73E60370-756D-45AD-A19A-C40A02DACCC7}
|
||||
|
||||
22
receiverUI/.github/copilot-instructions.md
vendored
22
receiverUI/.github/copilot-instructions.md
vendored
@@ -1,22 +0,0 @@
|
||||
---
|
||||
description: 'Answer questions about DevExpress UI Components and their APIs using the dxdocs server'
|
||||
---
|
||||
|
||||
You are a .NET programmer and DevExpress products expert.
|
||||
|
||||
You are tasked with answering questions about DevExpress components and their APIs using dxdocs MCP server tools.
|
||||
|
||||
For **ANY** question about DevExpress components, use the dxdocs server to construct your answer.
|
||||
|
||||
## Workflow:
|
||||
1. **Call devexpress_docs_search** to obtain help topics related to the user question
|
||||
2. **Call devexpress_docs_get_content** to fetch and read most relevant help topics
|
||||
3. **Reflect on obtained content** and how it relates to the question
|
||||
4. **Provide a comprehensive answer** based solely on retrieved information
|
||||
|
||||
## Constraints:
|
||||
- **Use devexpress_docs_search only once per question** to avoid redundant queries
|
||||
- When answering questions, **use only information obtained from MCP server tools**
|
||||
- **Include code examples** when available in the documentation
|
||||
- **Reference specific DevExpress controls and properties** mentioned in the documentation
|
||||
- **Invoke version-specific MCP tools** (for example, dxdocs25_1) if a user specifies a version (for example, v25.1)
|
||||
@@ -1,12 +0,0 @@
|
||||
{
|
||||
"servers": {
|
||||
"dxdocs": {
|
||||
"url": "https://api.devexpress.com/mcp/docs",
|
||||
"type": "http"
|
||||
},
|
||||
"dxdocs25_1": {
|
||||
"url": "https://api.devexpress.com/mcp/docs?v=25.1",
|
||||
"type": "http"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,126 +0,0 @@
|
||||
namespace EnvelopeGenerator.ReceiverUI.Web.Client.Api.Models;
|
||||
|
||||
/// <summary>
|
||||
/// Client-side DTOs for envelope display. These mirror the server-side
|
||||
/// EnvelopeReceiverDto / EnvelopeDto / DocumentDto used by the existing
|
||||
/// API endpoints, but expose only the fields the receiver UI actually needs.
|
||||
/// JSON serialization is camelCase by default (System.Text.Json).
|
||||
/// </summary>
|
||||
public class EnvelopeReceiverDto
|
||||
{
|
||||
public long EnvelopeId { get; set; }
|
||||
public long ReceiverId { get; set; }
|
||||
public string? Name { get; set; }
|
||||
public bool HasPhoneNumber { get; set; }
|
||||
public EnvelopeDto? Envelope { get; set; }
|
||||
public ReceiverDto? Receiver { get; set; }
|
||||
}
|
||||
|
||||
public class EnvelopeDto
|
||||
{
|
||||
public long Id { get; set; }
|
||||
public string? Uuid { get; set; }
|
||||
public string? Title { get; set; }
|
||||
public string? Message { get; set; }
|
||||
public bool ReadOnly { get; set; }
|
||||
public bool UseAccessCode { get; set; }
|
||||
public bool TFAEnabled { get; set; }
|
||||
public DateTime AddedWhen { get; set; }
|
||||
public SenderUserDto? User { get; set; }
|
||||
public List<DocumentDto>? Documents { get; set; }
|
||||
}
|
||||
|
||||
public class SenderUserDto
|
||||
{
|
||||
public string? Email { get; set; }
|
||||
public string? Prename { get; set; }
|
||||
public string? Name { get; set; }
|
||||
}
|
||||
|
||||
public class ReceiverDto
|
||||
{
|
||||
public long Id { get; set; }
|
||||
public string? EmailAddress { get; set; }
|
||||
public string? Signature { get; set; }
|
||||
public string? Prename { get; set; }
|
||||
public string? Name { get; set; }
|
||||
}
|
||||
|
||||
public class DocumentDto
|
||||
{
|
||||
public long Id { get; set; }
|
||||
public string? Name { get; set; }
|
||||
public List<DocumentElementDto>? Elements { get; set; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Signature placement on a PDF document. Pixel/inch units follow the same
|
||||
/// convention as the legacy PSPDFKit pipeline.
|
||||
/// </summary>
|
||||
public class DocumentElementDto
|
||||
{
|
||||
public int Id { get; set; }
|
||||
public int Page { get; set; }
|
||||
public double Left { get; set; }
|
||||
public double Top { get; set; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Body for <c>POST /api/readonly</c> (share read-only link with another e-mail).
|
||||
/// </summary>
|
||||
public class ReadOnlyShareRequest
|
||||
{
|
||||
public string ReceiverMail { get; set; } = string.Empty;
|
||||
public DateTime DateValid { get; set; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Returned by <c>GET /api/annotation/elements</c>. Each item describes a
|
||||
/// signature placeholder the authenticated receiver must sign on the
|
||||
/// current envelope. Coordinates are in PDF points relative to the page.
|
||||
/// </summary>
|
||||
public class SignatureElementDto
|
||||
{
|
||||
public int Id { get; set; }
|
||||
public int Page { get; set; }
|
||||
public double X { get; set; }
|
||||
public double Y { get; set; }
|
||||
public double Width { get; set; }
|
||||
public double Height { get; set; }
|
||||
public bool Required { get; set; }
|
||||
public string? Tooltip { get; set; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Body of <c>POST /api/annotation/blazor</c> — mirrors the API-side
|
||||
/// <c>BlazorSignaturePayload</c>.
|
||||
/// </summary>
|
||||
public class BlazorSignaturePayload
|
||||
{
|
||||
public List<BlazorSignatureEntry> Signatures { get; set; } = new();
|
||||
}
|
||||
|
||||
public class BlazorSignatureEntry
|
||||
{
|
||||
public int ElementId { get; set; }
|
||||
/// <summary>Image as data URL (e.g. <c>data:image/png;base64,...</c>).</summary>
|
||||
public string SignatureDataUrl { get; set; } = string.Empty;
|
||||
public string? Position { get; set; }
|
||||
public string? City { get; set; }
|
||||
public DateTime SignedAt { get; set; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Response of <c>GET /api/tfa/{envelopeReceiverId}</c>. The API serializes
|
||||
/// the anonymous type in <c>TfaRegistrationController.RegisterAsync</c>; the
|
||||
/// shape mirrored here is the camelCased JSON that crosses the wire.
|
||||
/// </summary>
|
||||
public class TfaRegistrationResponse
|
||||
{
|
||||
public int EnvelopeId { get; set; }
|
||||
public string? Uuid { get; set; }
|
||||
public string? Signature { get; set; }
|
||||
public DateTime? TfaRegDeadline { get; set; }
|
||||
/// <summary>Base64-encoded PNG suitable for <c>data:image/png;base64,...</c>.</summary>
|
||||
public string? TotpQR64 { get; set; }
|
||||
}
|
||||
@@ -1,53 +0,0 @@
|
||||
namespace EnvelopeGenerator.ReceiverUI.Web.Client.Api.Models;
|
||||
|
||||
/// <summary>
|
||||
/// Mirrors <c>EnvelopeGenerator.API.Models.ReceiverAuthResponse</c>.
|
||||
/// The Status string determines which fields are relevant.
|
||||
/// Possible values:
|
||||
/// requires_access_code, requires_tfa, show_document,
|
||||
/// already_signed, rejected, not_found, expired, error.
|
||||
/// </summary>
|
||||
public class ReceiverAuthResponse
|
||||
{
|
||||
public string Status { get; set; } = string.Empty;
|
||||
public string? Title { get; set; }
|
||||
public string? Message { get; set; }
|
||||
public string? SenderEmail { get; set; }
|
||||
public string? ReceiverName { get; set; }
|
||||
public bool TfaEnabled { get; set; }
|
||||
public bool HasPhoneNumber { get; set; }
|
||||
public bool ReadOnly { get; set; }
|
||||
|
||||
/// <summary>"sms" or "authenticator" when Status == "requires_tfa".</summary>
|
||||
public string? TfaType { get; set; }
|
||||
public DateTime? TfaExpiration { get; set; }
|
||||
public string? ErrorMessage { get; set; }
|
||||
}
|
||||
|
||||
public class AccessCodeRequest
|
||||
{
|
||||
public string AccessCode { get; set; } = string.Empty;
|
||||
public bool PreferSms { get; set; }
|
||||
}
|
||||
|
||||
public class TfaCodeRequest
|
||||
{
|
||||
public string Code { get; set; } = string.Empty;
|
||||
/// <summary>"sms" or "authenticator".</summary>
|
||||
public string Type { get; set; } = "authenticator";
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Status strings returned by <see cref="ReceiverAuthResponse.Status"/>.
|
||||
/// </summary>
|
||||
public static class ReceiverAuthStatus
|
||||
{
|
||||
public const string RequiresAccessCode = "requires_access_code";
|
||||
public const string RequiresTfa = "requires_tfa";
|
||||
public const string ShowDocument = "show_document";
|
||||
public const string AlreadySigned = "already_signed";
|
||||
public const string Rejected = "rejected";
|
||||
public const string NotFound = "not_found";
|
||||
public const string Expired = "expired";
|
||||
public const string Error = "error";
|
||||
}
|
||||
@@ -1,214 +0,0 @@
|
||||
using System.Net;
|
||||
using System.Net.Http.Json;
|
||||
using EnvelopeGenerator.ReceiverUI.Web.Client.Api.Models;
|
||||
|
||||
namespace EnvelopeGenerator.ReceiverUI.Web.Client.Api;
|
||||
|
||||
/// <summary>
|
||||
/// Typed HTTP client for the EnvelopeGenerator receiver API.
|
||||
/// All endpoints are routed through the BFF (same origin), so the
|
||||
/// authentication cookie set by the API is automatically attached
|
||||
/// by the browser to every request issued by the injected HttpClient.
|
||||
/// </summary>
|
||||
public class ReceiverApiClient
|
||||
{
|
||||
private readonly HttpClient _http;
|
||||
private readonly ILogger<ReceiverApiClient> _logger;
|
||||
|
||||
public ReceiverApiClient(HttpClient http, ILogger<ReceiverApiClient> logger)
|
||||
{
|
||||
_http = http;
|
||||
_logger = logger;
|
||||
}
|
||||
|
||||
// ?? Receiver Auth ????????????????????????????????????????????????
|
||||
|
||||
public Task<ReceiverAuthResponse?> GetStatusAsync(string envelopeKey, CancellationToken ct = default)
|
||||
=> GetAuthAsync($"api/receiverauth/{Uri.EscapeDataString(envelopeKey)}/status", ct);
|
||||
|
||||
public Task<ReceiverAuthResponse?> SubmitAccessCodeAsync(string envelopeKey, AccessCodeRequest req, CancellationToken ct = default)
|
||||
=> PostAuthAsync($"api/receiverauth/{Uri.EscapeDataString(envelopeKey)}/access-code", req, ct);
|
||||
|
||||
public Task<ReceiverAuthResponse?> SubmitTfaCodeAsync(string envelopeKey, TfaCodeRequest req, CancellationToken ct = default)
|
||||
=> PostAuthAsync($"api/receiverauth/{Uri.EscapeDataString(envelopeKey)}/tfa", req, ct);
|
||||
|
||||
// ?? Envelope Receiver ????????????????????????????????????????????
|
||||
|
||||
public async Task<EnvelopeReceiverDto?> GetEnvelopeReceiverAsync(string envelopeKey, CancellationToken ct = default)
|
||||
{
|
||||
var res = await _http.GetAsync($"api/envelopereceiver/{Uri.EscapeDataString(envelopeKey)}", ct);
|
||||
if (!res.IsSuccessStatusCode)
|
||||
return null;
|
||||
return await res.Content.ReadFromJsonAsync<EnvelopeReceiverDto>(cancellationToken: ct);
|
||||
}
|
||||
|
||||
/// <summary>Downloads the document bytes for the receiver to display in a PDF viewer.</summary>
|
||||
public async Task<byte[]?> GetDocumentAsync(string envelopeKey, CancellationToken ct = default)
|
||||
{
|
||||
var res = await _http.GetAsync($"api/document/{Uri.EscapeDataString(envelopeKey)}", ct);
|
||||
if (!res.IsSuccessStatusCode)
|
||||
return null;
|
||||
return await res.Content.ReadAsByteArrayAsync(ct);
|
||||
}
|
||||
|
||||
// ?? Annotation / Sign / Reject ???????????????????????????????????
|
||||
|
||||
/// <summary>
|
||||
/// Returns the signature placeholders the authenticated receiver must sign.
|
||||
/// </summary>
|
||||
public async Task<List<SignatureElementDto>> GetSignatureElementsAsync(CancellationToken ct = default)
|
||||
{
|
||||
try
|
||||
{
|
||||
return await _http.GetFromJsonAsync<List<SignatureElementDto>>("api/annotation/elements", ct)
|
||||
?? new List<SignatureElementDto>();
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogWarning(ex, "Failed to fetch signature elements.");
|
||||
return new List<SignatureElementDto>();
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Submits the signed envelope using the Blazor-friendly endpoint.
|
||||
/// </summary>
|
||||
public async Task<HttpStatusCode> SignBlazorAsync(BlazorSignaturePayload payload, CancellationToken ct = default)
|
||||
{
|
||||
var res = await _http.PostAsJsonAsync("api/annotation/blazor", payload, ct);
|
||||
return res.StatusCode;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Fetches the TOTP QR code + registration deadline for the given
|
||||
/// envelope-receiver key (encoded uuid+signature). The API generates
|
||||
/// a fresh secret on first call and persists it server-side.
|
||||
/// </summary>
|
||||
public async Task<(TfaRegistrationResponse? Data, HttpStatusCode Status)> GetTfaRegistrationAsync(string envelopeReceiverId, CancellationToken ct = default)
|
||||
{
|
||||
try
|
||||
{
|
||||
var res = await _http.GetAsync($"api/tfa/{Uri.EscapeDataString(envelopeReceiverId)}", ct);
|
||||
if (!res.IsSuccessStatusCode)
|
||||
return (null, res.StatusCode);
|
||||
var data = await res.Content.ReadFromJsonAsync<TfaRegistrationResponse>(cancellationToken: ct);
|
||||
return (data, res.StatusCode);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogWarning(ex, "Failed to fetch TFA registration for {Key}", envelopeReceiverId);
|
||||
return (null, HttpStatusCode.InternalServerError);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>Submits the signed annotations payload. Returns the HTTP status code.</summary>
|
||||
public async Task<HttpStatusCode> SignAsync<TPayload>(TPayload? payload, CancellationToken ct = default)
|
||||
{
|
||||
var res = payload is null
|
||||
? await _http.PostAsync("api/annotation", content: null, ct)
|
||||
: await _http.PostAsJsonAsync("api/annotation", payload, ct);
|
||||
return res.StatusCode;
|
||||
}
|
||||
|
||||
public async Task<bool> RejectAsync(string reason, CancellationToken ct = default)
|
||||
{
|
||||
var res = await _http.PostAsJsonAsync("api/annotation/reject", reason, ct);
|
||||
return res.IsSuccessStatusCode;
|
||||
}
|
||||
|
||||
// ?? Read-only share ??????????????????????????????????????????????
|
||||
|
||||
public async Task<bool> ShareReadOnlyAsync(ReadOnlyShareRequest req, CancellationToken ct = default)
|
||||
{
|
||||
var res = await _http.PostAsJsonAsync("api/readonly", req, ct);
|
||||
return res.IsSuccessStatusCode;
|
||||
}
|
||||
|
||||
// ?? Auth (logout) ????????????????????????????????????????????????
|
||||
|
||||
public async Task<bool> LogoutAsync(CancellationToken ct = default)
|
||||
{
|
||||
var res = await _http.PostAsync("auth/logout", content: null, ct);
|
||||
return res.IsSuccessStatusCode;
|
||||
}
|
||||
|
||||
// ?? Localization ?????????????????????????????????????????????????
|
||||
|
||||
public async Task<Dictionary<string, string>?> GetLocalizationStringsAsync(CancellationToken ct = default)
|
||||
{
|
||||
try
|
||||
{
|
||||
return await _http.GetFromJsonAsync<Dictionary<string, string>>("api/Localization", ct);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogWarning(ex, "Failed to fetch localization strings.");
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
public async Task SetLanguageAsync(string language, CancellationToken ct = default)
|
||||
{
|
||||
try
|
||||
{
|
||||
await _http.PostAsync($"api/Localization/lang?language={Uri.EscapeDataString(language)}", content: null, ct);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogWarning(ex, "Failed to set language to {Lang}.", language);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Returns the currently selected language code (e.g. "de", "en"), or
|
||||
/// <c>null</c> if no language cookie has been set yet (the API answers
|
||||
/// with HTTP 404 in that case).
|
||||
/// </summary>
|
||||
public async Task<string?> GetLanguageAsync(CancellationToken ct = default)
|
||||
{
|
||||
try
|
||||
{
|
||||
var res = await _http.GetAsync("api/Localization/lang", ct);
|
||||
if (!res.IsSuccessStatusCode)
|
||||
return null;
|
||||
return (await res.Content.ReadAsStringAsync(ct))?.Trim('"');
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogWarning(ex, "Failed to read current language.");
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
// ?? Helpers ??????????????????????????????????????????????????????
|
||||
|
||||
private async Task<ReceiverAuthResponse?> GetAuthAsync(string url, CancellationToken ct)
|
||||
{
|
||||
var res = await _http.GetAsync(url, ct);
|
||||
return await ReadAuthAsync(res, ct);
|
||||
}
|
||||
|
||||
private async Task<ReceiverAuthResponse?> PostAuthAsync<TReq>(string url, TReq body, CancellationToken ct)
|
||||
{
|
||||
var res = await _http.PostAsJsonAsync(url, body, ct);
|
||||
return await ReadAuthAsync(res, ct);
|
||||
}
|
||||
|
||||
private static async Task<ReceiverAuthResponse?> ReadAuthAsync(HttpResponseMessage res, CancellationToken ct)
|
||||
{
|
||||
// ReceiverAuthController returns a ReceiverAuthResponse body for both
|
||||
// 2xx and known error statuses (401/404/500). We always try to deserialize.
|
||||
try
|
||||
{
|
||||
return await res.Content.ReadFromJsonAsync<ReceiverAuthResponse>(cancellationToken: ct);
|
||||
}
|
||||
catch
|
||||
{
|
||||
return new ReceiverAuthResponse
|
||||
{
|
||||
Status = ReceiverAuthStatus.Error,
|
||||
ErrorMessage = $"Unexpected response ({(int)res.StatusCode})."
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
Binary file not shown.
@@ -1,37 +0,0 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk.BlazorWebAssembly">
|
||||
<PropertyGroup>
|
||||
<TargetFramework>net8.0</TargetFramework>
|
||||
<Nullable>enable</Nullable>
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
<NoDefaultLaunchSettingsFile>true</NoDefaultLaunchSettingsFile>
|
||||
<StaticWebAssetProjectMode>Default</StaticWebAssetProjectMode>
|
||||
<MSBuildWarningsAsMessages>$(MSBuildWarningsAsMessage);WASM0001</MSBuildWarningsAsMessages>
|
||||
<!-- Required so that native assets (libSkiaSharp, libHarfBuzzSharp) from
|
||||
SkiaSharp.NativeAssets.WebAssembly / HarfBuzzSharp.NativeAssets.WebAssembly
|
||||
are statically linked into the WASM runtime. Without this the
|
||||
DevExpress PDF SkiaRenderer throws DllNotFoundException: libSkiaSharp.
|
||||
Requires the "wasm-tools" workload: dotnet workload install wasm-tools -->
|
||||
<!--<WasmBuildNative>false</WasmBuildNative>-->
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="DevExpress.Blazor" Version="25.2.*" />
|
||||
<PackageReference Include="DevExpress.Blazor.PdfViewer" Version="25.2.*" />
|
||||
<PackageReference Include="DevExpress.Pdf.SkiaRenderer" Version="25.2.*" />
|
||||
<PackageReference Include="HarfBuzzSharp.NativeAssets.WebAssembly" Version="8.3.1.2" />
|
||||
<PackageReference Include="SkiaSharp.NativeAssets.WebAssembly" Version="3.119.1" />
|
||||
<PackageReference Include="DevExpress.AIIntegration.Blazor" Version="25.2.*" />
|
||||
<PackageReference Include="DevExpress.AIIntegration.OpenAI" Version="25.2.*" />
|
||||
|
||||
<PackageReference Include="Azure.AI.OpenAI" Version="2.2.0-beta.5" />
|
||||
<PackageReference Include="Microsoft.Extensions.AI" Version="9.7.1" />
|
||||
<PackageReference Include="Microsoft.Extensions.AI.OpenAI" Version="9.7.1-preview.1.25365.4" />
|
||||
|
||||
<PackageReference Include="Microsoft.AspNetCore.Components.WebAssembly" Version="8.0.20" />
|
||||
<PackageReference Include="Microsoft.Extensions.Http" Version="8.0.1" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<EmbeddedResource Include="Documents\Invoice.pdf" />
|
||||
</ItemGroup>
|
||||
</Project>
|
||||
@@ -1,38 +0,0 @@
|
||||
@inherits DrawerStateComponentBase
|
||||
|
||||
<div class="drawer-container">
|
||||
<DxDrawer PanelWidth="@PanelWidth"
|
||||
CssClass="@(CssClass + " mobile")"
|
||||
Mode="DrawerMode.Overlap"
|
||||
IsOpen="@ToggledDrawer"
|
||||
BodyTemplate="BodyTemplate"
|
||||
HeaderTemplate="HeaderTemplate"
|
||||
FooterTemplate="FooterTemplate"
|
||||
ApplyBackgroundShading="false"
|
||||
ClosedCssClass="panel-closed">
|
||||
<TargetContent>
|
||||
<DxDrawer PanelWidth="@PanelWidth"
|
||||
CssClass="@CssClass"
|
||||
Mode="DrawerMode.Shrink"
|
||||
IsOpen="@(!ToggledDrawer)"
|
||||
BodyTemplate="BodyTemplate"
|
||||
HeaderTemplate="HeaderTemplate"
|
||||
FooterTemplate="FooterTemplate"
|
||||
OpenCssClass="panel-open">
|
||||
<TargetContent>
|
||||
<div class="navigation-drawer-shading"></div>
|
||||
@TargetContent
|
||||
</TargetContent>
|
||||
</DxDrawer>
|
||||
</TargetContent>
|
||||
</DxDrawer>
|
||||
</div>
|
||||
|
||||
@code {
|
||||
[Parameter] public string? CssClass { get; set; }
|
||||
[Parameter] public string? PanelWidth { get; set; }
|
||||
[Parameter] public RenderFragment? TargetContent { get; set; }
|
||||
[Parameter] public RenderFragment? BodyTemplate { get; set; }
|
||||
[Parameter] public RenderFragment? HeaderTemplate { get; set; }
|
||||
[Parameter] public RenderFragment? FooterTemplate { get; set; }
|
||||
}
|
||||
@@ -1,53 +0,0 @@
|
||||
.drawer-container {
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.navigation-drawer-shading {
|
||||
height: 100%;
|
||||
position: absolute;
|
||||
transition: ease 300ms;
|
||||
transition-property: opacity, visibility;
|
||||
visibility: visible;
|
||||
width: 100%;
|
||||
z-index: 99;
|
||||
background-color: var(--dxds-color-surface-backdrop-default-rest);
|
||||
}
|
||||
|
||||
.navigation-drawer.mobile.panel-closed .navigation-drawer-shading {
|
||||
opacity: 0;
|
||||
visibility: hidden;
|
||||
}
|
||||
|
||||
::deep .navigation-drawer > .dxbl-drawer-panel {
|
||||
display: flex;
|
||||
}
|
||||
|
||||
::deep .navigation-drawer.mobile > .dxbl-drawer-panel {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.navigation-drawer-shading {
|
||||
display: none;
|
||||
}
|
||||
|
||||
::deep .panel-open:not(.mobile) .nav-buttons-container .menu-button {
|
||||
display: none;
|
||||
}
|
||||
|
||||
@media (max-width: 768px) {
|
||||
::deep .navigation-drawer > .dxbl-drawer-panel {
|
||||
display: none;
|
||||
}
|
||||
|
||||
::deep .navigation-drawer.mobile > .dxbl-drawer-panel {
|
||||
display: flex;
|
||||
}
|
||||
|
||||
.navigation-drawer-shading {
|
||||
display: block;
|
||||
}
|
||||
|
||||
::deep .panel-open:not(.mobile) .nav-buttons-container .menu-button {
|
||||
display: flex;
|
||||
}
|
||||
}
|
||||
@@ -1,53 +0,0 @@
|
||||
@inherits DrawerStateLayoutComponentBase
|
||||
@inject NavigationManager NavigationManager
|
||||
|
||||
<div class="page">
|
||||
<Drawer CssClass="navigation-drawer" PanelWidth="240px">
|
||||
<HeaderTemplate>
|
||||
<div class="navigation-drawer-header">
|
||||
<NavLink href="@AddDrawerStateToUrl("/")">
|
||||
<img class="logo" src="images/logo.svg" alt="DevExpress logo" />
|
||||
</NavLink>
|
||||
<NavLink aria-label="Close menu" href="@AddDrawerStateToUrlToggled(LocalPath)">
|
||||
<DxButton aria-label="Close menu" RenderStyle="ButtonRenderStyle.Light" RenderStyleMode="ButtonRenderStyleMode.Text" CssClass="btn-icon-only" IconCssClass="@(ToggledDrawer ? "icon icon-close" : "icon icon-menu")"></DxButton>
|
||||
</NavLink>
|
||||
</div>
|
||||
</HeaderTemplate>
|
||||
<BodyTemplate>
|
||||
<div class="w-100">
|
||||
<NavMenu></NavMenu>
|
||||
</div>
|
||||
</BodyTemplate>
|
||||
<FooterTemplate>
|
||||
<div class="navigation-drawer-footer">
|
||||
<NavLink href="https://docs.devexpress.com/Blazor/400725/blazor-components" class="button-link">
|
||||
<DxButton Text="Docs" RenderStyle="ButtonRenderStyle.Light" RenderStyleMode="ButtonRenderStyleMode.Text" IconCssClass="icon icon-docs"></DxButton>
|
||||
</NavLink>
|
||||
<NavLink href="https://demos.devexpress.com/blazor/" class="button-link">
|
||||
<DxButton Text="Demos" RenderStyle="ButtonRenderStyle.Light" RenderStyleMode="ButtonRenderStyleMode.Text" IconCssClass="icon icon-demos"></DxButton>
|
||||
</NavLink>
|
||||
</div>
|
||||
</FooterTemplate>
|
||||
<TargetContent>
|
||||
<div class="drawer-content">
|
||||
<div class="nav-buttons-container">
|
||||
<NavLink aria-label="Open menu" href="@AddDrawerStateToUrlToggled(LocalPath)" class="menu-button">
|
||||
<DxButton aria-label="Open menu" RenderStyle="ButtonRenderStyle.Secondary" RenderStyleMode="ButtonRenderStyleMode.Text" CssClass="btn-icon-only" IconCssClass="icon icon-menu"></DxButton>
|
||||
</NavLink>
|
||||
@if (LocalPath != "/") {
|
||||
<NavLink href="@AddDrawerStateToUrl("/")" class="button-link">
|
||||
<DxButton Text="Back to Home" RenderStyle="ButtonRenderStyle.Secondary" RenderStyleMode="ButtonRenderStyleMode.Text" CssClass="back-button" IconCssClass="icon icon-back"></DxButton>
|
||||
</NavLink>
|
||||
}
|
||||
</div>
|
||||
<div class="page-content-container">
|
||||
@Body
|
||||
</div>
|
||||
</div>
|
||||
</TargetContent>
|
||||
</Drawer>
|
||||
</div>
|
||||
|
||||
@code {
|
||||
string LocalPath => new Uri(NavigationManager.Uri).LocalPath;
|
||||
}
|
||||
@@ -1,67 +0,0 @@
|
||||
.page {
|
||||
height: 100%;
|
||||
min-height: 0;
|
||||
}
|
||||
|
||||
::deep .navigation-drawer {
|
||||
--dxbl-drawer-panel-body-padding-x: 0;
|
||||
--dxbl-drawer-panel-body-padding-y: 1rem;
|
||||
--dxbl-drawer-panel-footer-bg: none;
|
||||
--dxbl-drawer-panel-header-bg: none;
|
||||
--dxbl-drawer-separator-border-width: 0;
|
||||
}
|
||||
|
||||
::deep .navigation-drawer > .dxbl-drawer-panel {
|
||||
background-image: linear-gradient(180deg, var(--dxds-color-surface-primary-default-rest) 0%, var(--dxds-primary-170) 150%);
|
||||
}
|
||||
|
||||
.navigation-drawer-header {
|
||||
align-items: center;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
padding: 1.375rem 0.375rem;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.navigation-drawer-header .logo {
|
||||
height: 1.5rem;
|
||||
width: 9rem;
|
||||
}
|
||||
|
||||
.navigation-drawer-footer {
|
||||
display: flex;
|
||||
justify-content: space-evenly;
|
||||
padding-bottom: 0.875rem;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.drawer-content {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
height: 100%;
|
||||
overflow: auto;
|
||||
padding: 2rem 1.5rem 1.5rem 1.5rem;
|
||||
}
|
||||
|
||||
.nav-buttons-container {
|
||||
align-items: center;
|
||||
display: flex;
|
||||
gap: 0.625rem;
|
||||
min-height: 2rem;
|
||||
}
|
||||
|
||||
::deep .nav-buttons-container > a {
|
||||
color: inherit;
|
||||
}
|
||||
|
||||
::deep .nav-buttons-container .back-button {
|
||||
padding-left: 0;
|
||||
padding-right: 0.25rem;
|
||||
}
|
||||
|
||||
.page-content-container {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
flex-grow: 1;
|
||||
min-height: 0;
|
||||
}
|
||||
@@ -1,35 +0,0 @@
|
||||
@inject NavigationManager NavigationManager
|
||||
@implements IDisposable
|
||||
|
||||
<div>
|
||||
<DxMenu Orientation="@Orientation.Vertical" CssClass="menu">
|
||||
<Items>
|
||||
<DxMenuItem NavigateUrl="/samples" Text="Samples" CssClass="@MenuItemCssClass("/samples")" IconCssClass="icon icon-home"></DxMenuItem>
|
||||
<DxMenuItem NavigateUrl="/counter" Text="Counter" CssClass="@MenuItemCssClass("/counter")" IconCssClass="icon icon-counter"></DxMenuItem>
|
||||
<DxMenuItem NavigateUrl="/weather" Text="Weather" CssClass="@MenuItemCssClass("/weather")" IconCssClass="icon icon-weather"></DxMenuItem>
|
||||
<DxMenuItem NavigateUrl="/pdfviewer" Text="PDF Viewer" CssClass="@MenuItemCssClass("/pdfviewer")" IconCssClass="icon icon-pdf-viewer"></DxMenuItem>
|
||||
</Items>
|
||||
</DxMenu>
|
||||
</div>
|
||||
|
||||
@code {
|
||||
private string? currentLocalPath;
|
||||
|
||||
protected override void OnInitialized() {
|
||||
currentLocalPath = new Uri(NavigationManager.Uri).LocalPath;
|
||||
NavigationManager.LocationChanged += OnLocationChanged;
|
||||
}
|
||||
|
||||
private void OnLocationChanged(object? sender, LocationChangedEventArgs e) {
|
||||
currentLocalPath = new Uri(NavigationManager.Uri).LocalPath;
|
||||
InvokeAsync(StateHasChanged);
|
||||
}
|
||||
|
||||
private string? MenuItemCssClass(string itemPath) {
|
||||
return string.Equals(currentLocalPath, itemPath, StringComparison.OrdinalIgnoreCase) ? "menu-item-active" : null;
|
||||
}
|
||||
|
||||
public void Dispose() {
|
||||
NavigationManager.LocationChanged -= OnLocationChanged;
|
||||
}
|
||||
}
|
||||
@@ -1,23 +0,0 @@
|
||||
::deep .menu {
|
||||
--dxbl-menu-bottom-left-border-radius: 0;
|
||||
--dxbl-menu-bottom-right-border-radius: 0;
|
||||
--dxbl-menu-top-left-border-radius: 0;
|
||||
--dxbl-menu-top-right-border-radius: 0;
|
||||
--dxbl-menu-item-padding-x: 1.125rem;
|
||||
--dxbl-menu-item-padding-y: 0.5rem;
|
||||
--dxbl-menu-item-color: var(--dxds-color-content-neutral-default-static-dark-rest);
|
||||
--dxbl-menu-item-image-color: var(--dxds-color-content-neutral-default-static-dark-rest);
|
||||
--dxbl-menu-item-hover-bg: rgb(from var(--dxds-color-surface-neutral-default-static-light-rest) r g b / 0.15);
|
||||
--dxbl-menu-item-hover-color: var(--dxds-color-content-neutral-default-static-dark-hovered);
|
||||
--dxbl-menu-item-hover-image-color: var(--dxds-color-content-neutral-default-static-dark-hovered);
|
||||
|
||||
background: none;
|
||||
}
|
||||
|
||||
::deep .menu.display-mobile {
|
||||
margin-bottom: 2rem;
|
||||
}
|
||||
|
||||
::deep .menu-item-active {
|
||||
background-color: rgb(from var(--dxds-color-surface-neutral-default-static-light-rest) r g b / 0.05);
|
||||
}
|
||||
@@ -1,116 +0,0 @@
|
||||
@inherits LayoutComponentBase
|
||||
@implements IDisposable
|
||||
@inject LocalizationService Loc
|
||||
@inject NavigationManager Nav
|
||||
@inject IJSRuntime JS
|
||||
|
||||
@*
|
||||
Layout for the receiver-facing pages migrated from EnvelopeGenerator.Web.
|
||||
Mirrors the legacy MVC <body> structure: page content + sticky footer
|
||||
with copyright link, language switcher and privacy link.
|
||||
|
||||
Cookie consent is reimplemented in Blazor (localStorage-backed) because
|
||||
ASP.NET Core's <ITrackingConsentFeature> only works on the server side
|
||||
and is awkward to integrate with InteractiveAuto rendering.
|
||||
*@
|
||||
|
||||
<div class="receiver-shell">
|
||||
|
||||
@* Main page content *@
|
||||
<main role="main" class="flex-grow-1">
|
||||
@Body
|
||||
</main>
|
||||
|
||||
@* Cookie consent banner (Blazor counterpart of _CookieConsentPartial). *@
|
||||
@if (_consentVisible)
|
||||
{
|
||||
<div class="receiver-cookie-banner" role="alertdialog" aria-live="polite">
|
||||
<span>@Loc["CookieConsentMessage"]</span>
|
||||
<DxButton Text="@Loc["Accept"]"
|
||||
RenderStyle="ButtonRenderStyle.Primary"
|
||||
Click="AcceptCookiesAsync" />
|
||||
</div>
|
||||
}
|
||||
|
||||
@* Footer (copyright + language switcher + privacy). *@
|
||||
<footer class="receiver-footer">
|
||||
<span>
|
||||
© SignFlow 2023-@DateTime.Now.Year
|
||||
<a href="https://digitaldata.works" target="_blank" rel="noopener">Digital Data GmbH</a>
|
||||
</span>
|
||||
|
||||
<div class="d-flex align-items-center gap-2">
|
||||
<select class="language-switcher" value="@_currentLang" @onchange="OnLanguageChangedAsync">
|
||||
@foreach (var (lang, native, _) in LocalizationService.SupportedLanguages)
|
||||
{
|
||||
<option value="@lang" selected="@(lang == _currentLang)">@native</option>
|
||||
}
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<a href="@($"/privacy-policy.{_currentLang}.html")" target="_blank" rel="noopener">
|
||||
@Loc["Privacy"]
|
||||
</a>
|
||||
</footer>
|
||||
</div>
|
||||
|
||||
@code {
|
||||
private string _currentLang = "de";
|
||||
private bool _consentVisible;
|
||||
|
||||
protected override async Task OnInitializedAsync()
|
||||
{
|
||||
Loc.Changed += OnLocChanged;
|
||||
await Loc.EnsureLoadedAsync();
|
||||
_currentLang = Loc.CurrentLanguage ?? "de";
|
||||
}
|
||||
|
||||
protected override async Task OnAfterRenderAsync(bool firstRender)
|
||||
{
|
||||
if (!firstRender) return;
|
||||
// Probe localStorage on the client only — InteractiveAuto means
|
||||
// the server prerender runs without a browser, so JS interop is
|
||||
// unavailable until the first client render.
|
||||
try
|
||||
{
|
||||
var accepted = await JS.InvokeAsync<string?>("localStorage.getItem", "receiver.cookie-consent");
|
||||
_consentVisible = accepted != "1";
|
||||
StateHasChanged();
|
||||
}
|
||||
catch
|
||||
{
|
||||
// No-op: server prerender (JS unavailable) keeps the banner hidden.
|
||||
}
|
||||
}
|
||||
|
||||
private async Task AcceptCookiesAsync()
|
||||
{
|
||||
try
|
||||
{
|
||||
await JS.InvokeVoidAsync("localStorage.setItem", "receiver.cookie-consent", "1");
|
||||
}
|
||||
catch { /* ignore */ }
|
||||
_consentVisible = false;
|
||||
}
|
||||
|
||||
private async Task OnLanguageChangedAsync(ChangeEventArgs e)
|
||||
{
|
||||
var code = e.Value?.ToString();
|
||||
if (string.IsNullOrEmpty(code) || code == _currentLang)
|
||||
return;
|
||||
|
||||
_currentLang = code;
|
||||
await Loc.ChangeLanguageAsync(code);
|
||||
// Force a full reload so ASP.NET Core localization middleware
|
||||
// picks up the new culture for any subsequent SSR / API calls.
|
||||
Nav.NavigateTo(Nav.Uri, forceLoad: true);
|
||||
}
|
||||
|
||||
private void OnLocChanged()
|
||||
{
|
||||
_currentLang = Loc.CurrentLanguage ?? _currentLang;
|
||||
InvokeAsync(StateHasChanged);
|
||||
}
|
||||
|
||||
public void Dispose() => Loc.Changed -= OnLocChanged;
|
||||
}
|
||||
@@ -1,24 +0,0 @@
|
||||
@page "/counter"
|
||||
|
||||
<PageTitle>Counter</PageTitle>
|
||||
|
||||
<h1>Counter</h1>
|
||||
|
||||
<div class="counter-block">
|
||||
<div class="counter-content">
|
||||
<div class="counter-count">
|
||||
@currentCount
|
||||
</div>
|
||||
current count
|
||||
</div>
|
||||
<DxButton Click="IncrementCount">Click me</DxButton>
|
||||
</div>
|
||||
|
||||
@code {
|
||||
private int currentCount;
|
||||
|
||||
private void IncrementCount()
|
||||
{
|
||||
currentCount++;
|
||||
}
|
||||
}
|
||||
@@ -1,25 +0,0 @@
|
||||
.counter-block {
|
||||
align-items: center;
|
||||
border-radius: 1rem;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 1.5rem;
|
||||
height: 17rem;
|
||||
justify-content: center;
|
||||
padding: 2.5rem 1.5rem 1.5rem;
|
||||
width: 16.875rem;
|
||||
border: 1px solid var(--dxds-color-border-neutral-default-rest);
|
||||
}
|
||||
|
||||
.counter-block .counter-content {
|
||||
align-items: center;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 0.5rem;
|
||||
}
|
||||
|
||||
.counter-block .counter-count {
|
||||
font-size: 7.5rem;
|
||||
font-weight: 400;
|
||||
line-height: 7.75rem;
|
||||
}
|
||||
@@ -1,27 +0,0 @@
|
||||
@page "/samples"
|
||||
|
||||
<PageTitle>Welcome</PageTitle>
|
||||
|
||||
<div class="main-content">
|
||||
<DxGridLayout CssClass="index-gridlayout">
|
||||
<Rows>
|
||||
<DxGridLayoutRow Height="auto" Areas="header"></DxGridLayoutRow>
|
||||
<DxGridLayoutRow Height="auto" Areas="tiles"></DxGridLayoutRow>
|
||||
</Rows>
|
||||
<Items>
|
||||
<DxGridLayoutItem Area="header" CssClass="title">
|
||||
<Template>
|
||||
<div class="title-header-text">Hello World!</div>
|
||||
<div class="title-content-text">Welcome to your new DevExpress Blazor Application</div>
|
||||
</Template>
|
||||
</DxGridLayoutItem>
|
||||
<DxGridLayoutItem Area="tiles" CssClass="tiles">
|
||||
<Template>
|
||||
<IndexTile NavigateUrl="/counter" Title="Counter" Description="Count mouse clicks and track the total." IconCssClass="icon-counter" />
|
||||
<IndexTile NavigateUrl="/weather" Title="Weather" Description="See a 5-day temperature and weather conditions forecast." IconCssClass="icon-weather" />
|
||||
<IndexTile NavigateUrl="/pdfviewer" Title="PDF Viewer" Description="Load PDF files and view them in the web browser." IconCssClass="icon-pdf-viewer" />
|
||||
</Template>
|
||||
</DxGridLayoutItem>
|
||||
</Items>
|
||||
</DxGridLayout>
|
||||
</div>
|
||||
@@ -1,31 +0,0 @@
|
||||
::deep .index-gridlayout {
|
||||
container-type: inline-size;
|
||||
height: auto;
|
||||
margin-top: auto;
|
||||
margin-bottom: auto;
|
||||
padding-bottom: 9rem;
|
||||
}
|
||||
|
||||
::deep .title {
|
||||
padding-bottom: 3rem;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
::deep .tiles {
|
||||
--tile-column-count: 4;
|
||||
|
||||
display: grid;
|
||||
gap: 1rem;
|
||||
grid-template-columns: repeat(var(--tile-column-count), max-content);
|
||||
justify-content: center;
|
||||
|
||||
@container (max-width: 60.5rem) {
|
||||
--tile-column-count: 3;
|
||||
}
|
||||
@container (max-width: 45.125rem) {
|
||||
--tile-column-count: 2;
|
||||
}
|
||||
@container (max-width: 29.75rem) {
|
||||
--tile-column-count: 1;
|
||||
}
|
||||
}
|
||||
@@ -1,40 +0,0 @@
|
||||
@inherits DrawerStateComponentBase
|
||||
|
||||
<div class="tile">
|
||||
<NavLink href="@AddDrawerStateToUrl(NavigateUrl)">
|
||||
<DxGridLayout CssClass="tile-content" ColumnSpacing="0.75rem" RowSpacing="0.75rem">
|
||||
<Rows>
|
||||
<DxGridLayoutRow Areas="icon title" Height="auto" />
|
||||
<DxGridLayoutRow Areas="description description" Height="auto" />
|
||||
</Rows>
|
||||
<Columns>
|
||||
<DxGridLayoutColumn Width="auto" />
|
||||
<DxGridLayoutColumn />
|
||||
</Columns>
|
||||
<Items>
|
||||
<DxGridLayoutItem Area="icon" CssClass="tile-icon">
|
||||
<Template>
|
||||
<div class="@("icon " + IconCssClass)" aria-hidden="true"></div>
|
||||
</Template>
|
||||
</DxGridLayoutItem>
|
||||
<DxGridLayoutItem Area="title" CssClass="tile-title">
|
||||
<Template>
|
||||
@Title
|
||||
</Template>
|
||||
</DxGridLayoutItem>
|
||||
<DxGridLayoutItem Area="description" CssClass="tile-description">
|
||||
<Template>
|
||||
@Description
|
||||
</Template>
|
||||
</DxGridLayoutItem>
|
||||
</Items>
|
||||
</DxGridLayout>
|
||||
</NavLink>
|
||||
</div>
|
||||
|
||||
@code {
|
||||
[Parameter] public string NavigateUrl { get; set; } = string.Empty;
|
||||
[Parameter] public string? Title { get; set; }
|
||||
[Parameter] public string? Description { get; set; }
|
||||
[Parameter] public string? IconCssClass { get; set; }
|
||||
}
|
||||
@@ -1,44 +0,0 @@
|
||||
.tile {
|
||||
border-radius: 0.75rem;
|
||||
height: 7.5rem;
|
||||
transition: box-shadow 0.2s;
|
||||
width: 14.375rem;
|
||||
border: 1px solid var(--dxds-color-border-neutral-default-rest);
|
||||
}
|
||||
|
||||
.tile:hover {
|
||||
box-shadow: 0 4px 8px 0 rgba(170, 170, 170, 0.24), 0 0 2px 0 rgba(170, 170, 170, 0.2);
|
||||
}
|
||||
|
||||
.tile ::deep > a {
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
::deep .tile-content {
|
||||
padding: 1rem;
|
||||
}
|
||||
|
||||
::deep .tile-icon {
|
||||
border-radius: 0.375rem;
|
||||
height: 2.75rem;
|
||||
padding: 0.75rem;
|
||||
width: 2.75rem;
|
||||
border: 1px solid var(--dxds-color-border-neutral-default-rest);
|
||||
}
|
||||
|
||||
::deep .tile-title {
|
||||
align-self: center;
|
||||
color: var(--dxds-color-content-neutral-default-rest);
|
||||
font-size: var(--dxds-font-size-base-md);
|
||||
font-weight: var(--dxds-font-weight-base-strong);
|
||||
letter-spacing: var(--dxds-letter-spacing-base-md);
|
||||
line-height: var(--dxds-line-height-base-md);
|
||||
}
|
||||
|
||||
::deep .tile-description {
|
||||
color: var(--dxds-color-content-neutral-subdued-rest);
|
||||
font-size: var(--dxds-font-size-base-sm);
|
||||
font-weight: var(--dxds-font-weight-base-default);
|
||||
letter-spacing: var(--dxds-letter-spacing-base-sm);
|
||||
line-height: var(--dxds-line-height-base-sm);
|
||||
}
|
||||
@@ -1,22 +0,0 @@
|
||||
@page "/pdfviewer"
|
||||
|
||||
<PageTitle>PDF Viewer</PageTitle>
|
||||
|
||||
<h1>PDF Viewer</h1>
|
||||
|
||||
<DxPdfViewer CssClass="h-100 w-100" DocumentContent="DocumentContent" ZoomLevel="1" />
|
||||
|
||||
@code {
|
||||
byte[]? DocumentContent { get; set; }
|
||||
|
||||
protected override async Task OnInitializedAsync() {
|
||||
await base.OnInitializedAsync();
|
||||
|
||||
await using Stream stream =
|
||||
System.Reflection.Assembly.GetExecutingAssembly().GetManifestResourceStream("EnvelopeGenerator.ReceiverUI.Web.Client.Documents.Invoice.pdf")
|
||||
?? throw new InvalidOperationException("Resource not found.");
|
||||
using MemoryStream ms = new();
|
||||
await stream.CopyToAsync(ms);
|
||||
DocumentContent = ms.ToArray();
|
||||
}
|
||||
}
|
||||
@@ -1,24 +0,0 @@
|
||||
@page "/envelope-expired"
|
||||
@layout EnvelopeGenerator.ReceiverUI.Web.Client.Layout.ReceiverLayout
|
||||
@rendermode InteractiveAuto
|
||||
@inject LocalizationService Loc
|
||||
|
||||
@*
|
||||
Counterpart of Views/Envelope/EnvelopeExpired.cshtml.
|
||||
*@
|
||||
|
||||
<PageTitle>@Loc["Expired"]</PageTitle>
|
||||
|
||||
<div class="page container p-5">
|
||||
<header class="text-center">
|
||||
<div class="icon expired"></div>
|
||||
<h1>@Loc["Expired"]</h1>
|
||||
</header>
|
||||
<section class="text-center">
|
||||
<p>@Loc["DocumentSharingPeriodExpired"]</p>
|
||||
</section>
|
||||
</div>
|
||||
|
||||
@code {
|
||||
protected override async Task OnInitializedAsync() => await Loc.EnsureLoadedAsync();
|
||||
}
|
||||
@@ -1,20 +0,0 @@
|
||||
@page "/envelopekey/{*Path}"
|
||||
@layout EnvelopeGenerator.ReceiverUI.Web.Client.Layout.ReceiverLayout
|
||||
@rendermode InteractiveAuto
|
||||
@inject NavigationManager Nav
|
||||
|
||||
@*
|
||||
Counterpart of EnvelopeKeyRedirController:
|
||||
/EnvelopeKey/{*path} ? /envelope/{path}
|
||||
Preserves backwards compatibility with links generated by older e-mails.
|
||||
*@
|
||||
|
||||
@code {
|
||||
[Parameter] public string? Path { get; set; }
|
||||
|
||||
protected override void OnInitialized()
|
||||
{
|
||||
var target = "/envelope/" + (Path ?? string.Empty).TrimStart('/');
|
||||
Nav.NavigateTo(target, replace: true);
|
||||
}
|
||||
}
|
||||
@@ -1,229 +0,0 @@
|
||||
@implements IDisposable
|
||||
@inject ReceiverApiClient Api
|
||||
@inject ReceiverAuthState State
|
||||
@inject LocalizationService Loc
|
||||
|
||||
@*
|
||||
Counterpart of EnvelopeGenerator.Web/Views/Envelope/EnvelopeLocked.cshtml.
|
||||
|
||||
Renders one of three input modes based on the current auth state:
|
||||
|
||||
• Status == requires_access_code
|
||||
? AccessCode input (+ optional "2FA per SMS" toggle)
|
||||
|
||||
• Status == requires_tfa, TfaType == "sms"
|
||||
? SMS code input + countdown until TfaExpiration
|
||||
|
||||
• Status == requires_tfa, TfaType == "authenticator"
|
||||
? Authenticator code input + "set up authenticator" link
|
||||
|
||||
On submit, the matching ReceiverApiClient method is invoked. The fresh
|
||||
response replaces ReceiverAuthState.Current; the parent EnvelopePage
|
||||
re-renders and either shows the document or navigates to a terminal page.
|
||||
*@
|
||||
|
||||
<div class="page container py-4 px-4">
|
||||
|
||||
@* — Welcome banner (custom company image is added in Phase 6) — *@
|
||||
<header class="text-center">
|
||||
<div class="header-1 alert alert-secondary" role="alert">
|
||||
<h3 class="text">@Loc["WelcomeToTheESignPortal"]</h3>
|
||||
</div>
|
||||
|
||||
<div class="icon locked @(IsTfa ? "tfa" : "") mt-4 mb-1">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="72" height="72" fill="currentColor"
|
||||
class="bi bi-shield-lock" viewBox="0 0 16 16">
|
||||
<path d="M5.338 1.59a61 61 0 0 0-2.837.856.48.48 0 0 0-.328.39c-.554 4.157.726 7.19 2.253 9.188a10.7 10.7 0 0 0 2.287 2.233c.346.244.652.42.893.533q.18.085.293.118a1 1 0 0 0 .101.025 1 1 0 0 0 .1-.025q.114-.034.294-.118c.24-.113.547-.29.893-.533a10.7 10.7 0 0 0 2.287-2.233c1.527-1.997 2.807-5.031 2.253-9.188a.48.48 0 0 0-.328-.39c-.651-.213-1.75-.56-2.837-.855C9.552 1.29 8.531 1.067 8 1.067c-.53 0-1.552.223-2.662.524zM5.072.56C6.157.265 7.31 0 8 0s1.843.265 2.928.56c1.11.3 2.229.655 2.887.87a1.54 1.54 0 0 1 1.044 1.262c.596 4.477-.787 7.795-2.465 9.99a11.8 11.8 0 0 1-2.517 2.453 7 7 0 0 1-1.048.625c-.28.132-.581.24-.829.24s-.548-.108-.829-.24a7 7 0 0 1-1.048-.625 11.8 11.8 0 0 1-2.517-2.453C1.928 10.487.545 7.169 1.141 2.692A1.54 1.54 0 0 1 2.185 1.43 63 63 0 0 1 5.072.56" />
|
||||
<path d="M9.5 6.5a1.5 1.5 0 0 1-1 1.415l.385 1.99a.5.5 0 0 1-.491.595h-.788a.5.5 0 0 1-.49-.595l.384-1.99a1.5 1.5 0 1 1 2-1.415" />
|
||||
</svg>
|
||||
</div>
|
||||
<h1>@Loc[$"LockedTitle{CodeKey}"]</h1>
|
||||
</header>
|
||||
|
||||
@* — "Set up authenticator" hint, shown only on the authenticator step — *@
|
||||
@if (IsAuthenticator)
|
||||
{
|
||||
<section class="text-center">
|
||||
<p class="m-0 p-0">
|
||||
@Loc["AuthenticatorSetup_Prefix"]
|
||||
<a class="icon-link m-0 p-0" href="@($"/tfa/{EnvelopeKey}")" target="_blank" style="text-decoration:none">
|
||||
@Loc["AuthenticatorSetup_Link"] <i class="bi bi-box-arrow-up-right"></i>
|
||||
</a>
|
||||
@Loc["AuthenticatorSetup_Suffix"]
|
||||
</p>
|
||||
</section>
|
||||
}
|
||||
|
||||
<section class="text-center">
|
||||
<p>@Loc[$"LockedBody{CodeKey}"]</p>
|
||||
</section>
|
||||
|
||||
<div class="row m-0 p-0">
|
||||
<div class="access-code-panel justify-content-center align-items-center p-0 m-0">
|
||||
<EditForm Model="this" OnValidSubmit="HandleSubmit" Context="editContext"
|
||||
id="form-access-code" class="form form-floating mb-0">
|
||||
<div class="form-floating access-code-form-floating">
|
||||
|
||||
<input id="access_code" type="password" class="form-control"
|
||||
placeholder="@Loc[$"LockedCodeLabel{CodeKey}"]"
|
||||
@bind="Code" @bind:event="oninput"
|
||||
disabled="@_submitting" required />
|
||||
<label for="access_code">@Loc[$"LockedCodeLabel{CodeKey}"]</label>
|
||||
|
||||
<DxButton SubmitFormOnClick="true" Enabled="@(!_submitting)"
|
||||
RenderStyle="ButtonRenderStyle.Primary"
|
||||
CssClass="btn btn-primary">
|
||||
<span class="material-symbols-outlined">login</span>
|
||||
</DxButton>
|
||||
|
||||
@if (ShowSmsToggle)
|
||||
{
|
||||
<div class="form-check form-switch tfa-sms">
|
||||
<input class="form-check-input" type="checkbox" role="switch"
|
||||
id="flexSwitchCheckChecked"
|
||||
@bind="PreferSms"
|
||||
disabled="@(!State.Current!.HasPhoneNumber)" />
|
||||
<label class="form-check-label" for="flexSwitchCheckChecked">2FA per SMS</label>
|
||||
</div>
|
||||
}
|
||||
|
||||
@if (IsSms && _smsRemaining is not null)
|
||||
{
|
||||
<div id="sms-timer" class="alert alert-primary" role="alert">@_smsRemaining</div>
|
||||
}
|
||||
</div>
|
||||
</EditForm>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@if (!string.IsNullOrEmpty(State.Current?.ErrorMessage))
|
||||
{
|
||||
<div id="access-code-error-message" class="alert alert-danger row" role="alert">
|
||||
@State.Current.ErrorMessage
|
||||
</div>
|
||||
}
|
||||
|
||||
<section class="no-receiver-explanation text-center">
|
||||
<details>
|
||||
<summary>@Loc[$"LockedFooterTitle{CodeKey}"]</summary>
|
||||
<p>
|
||||
@Loc.Format($"LockedFooterBody{CodeKey}",
|
||||
State.Current?.SenderEmail ?? string.Empty,
|
||||
$"Envelope - {State.Current?.Title}",
|
||||
string.Empty)
|
||||
</p>
|
||||
</details>
|
||||
</section>
|
||||
</div>
|
||||
|
||||
@code {
|
||||
[Parameter] public string EnvelopeKey { get; set; } = string.Empty;
|
||||
|
||||
private string Code { get; set; } = string.Empty;
|
||||
private bool PreferSms { get; set; }
|
||||
private bool _submitting;
|
||||
private System.Threading.Timer? _smsTimer;
|
||||
private string? _smsRemaining;
|
||||
|
||||
// — Mode helpers ????????????????????????????????????????????????
|
||||
private bool IsAccessCodeStep => State.Current?.Status == ReceiverAuthStatus.RequiresAccessCode;
|
||||
private bool IsTfa => State.Current?.Status == ReceiverAuthStatus.RequiresTfa;
|
||||
private bool IsSms => IsTfa && State.Current?.TfaType == "sms";
|
||||
private bool IsAuthenticator => IsTfa && State.Current?.TfaType == "authenticator";
|
||||
|
||||
/// <summary>
|
||||
/// Mirrors the legacy view's "codeKeyName" suffix used to pick the right
|
||||
/// resource string ("LockedTitleAccess", "LockedTitleSms", ...).
|
||||
/// </summary>
|
||||
private string CodeKey => IsSms ? "Sms" : IsAuthenticator ? "Authenticator" : "Access";
|
||||
|
||||
private bool ShowSmsToggle =>
|
||||
IsAccessCodeStep
|
||||
&& (State.Current?.TfaEnabled ?? false);
|
||||
|
||||
protected override async Task OnInitializedAsync()
|
||||
{
|
||||
await Loc.EnsureLoadedAsync();
|
||||
State.Changed += OnStateChanged;
|
||||
ResetSmsTimer();
|
||||
}
|
||||
|
||||
private void OnStateChanged()
|
||||
{
|
||||
ResetSmsTimer();
|
||||
InvokeAsync(StateHasChanged);
|
||||
}
|
||||
|
||||
private async Task HandleSubmit()
|
||||
{
|
||||
if (_submitting || string.IsNullOrWhiteSpace(Code))
|
||||
return;
|
||||
|
||||
_submitting = true;
|
||||
try
|
||||
{
|
||||
ReceiverAuthResponse? res;
|
||||
if (IsAccessCodeStep)
|
||||
{
|
||||
res = await Api.SubmitAccessCodeAsync(EnvelopeKey, new AccessCodeRequest
|
||||
{
|
||||
AccessCode = Code,
|
||||
PreferSms = PreferSms
|
||||
});
|
||||
}
|
||||
else // TFA step
|
||||
{
|
||||
res = await Api.SubmitTfaCodeAsync(EnvelopeKey, new TfaCodeRequest
|
||||
{
|
||||
Code = Code,
|
||||
Type = State.Current?.TfaType ?? "authenticator"
|
||||
});
|
||||
}
|
||||
|
||||
Code = string.Empty;
|
||||
State.Set(EnvelopeKey, res);
|
||||
}
|
||||
finally
|
||||
{
|
||||
_submitting = false;
|
||||
}
|
||||
}
|
||||
|
||||
// — SMS countdown ???????????????????????????????????????????????
|
||||
private void ResetSmsTimer()
|
||||
{
|
||||
_smsTimer?.Dispose();
|
||||
_smsTimer = null;
|
||||
_smsRemaining = null;
|
||||
|
||||
if (!IsSms || State.Current?.TfaExpiration is not DateTime exp)
|
||||
return;
|
||||
|
||||
UpdateRemaining(exp);
|
||||
_smsTimer = new System.Threading.Timer(_ =>
|
||||
{
|
||||
UpdateRemaining(exp);
|
||||
InvokeAsync(StateHasChanged);
|
||||
}, null, 1000, 1000);
|
||||
}
|
||||
|
||||
private void UpdateRemaining(DateTime expiration)
|
||||
{
|
||||
var diff = expiration - DateTime.Now;
|
||||
if (diff <= TimeSpan.Zero)
|
||||
{
|
||||
_smsRemaining = "00:00";
|
||||
_smsTimer?.Dispose();
|
||||
_smsTimer = null;
|
||||
return;
|
||||
}
|
||||
_smsRemaining = $"{(int)diff.TotalMinutes:00}:{diff.Seconds:00}";
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
State.Changed -= OnStateChanged;
|
||||
_smsTimer?.Dispose();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,24 +0,0 @@
|
||||
@page "/envelope-not-found"
|
||||
@layout EnvelopeGenerator.ReceiverUI.Web.Client.Layout.ReceiverLayout
|
||||
@rendermode InteractiveAuto
|
||||
@inject LocalizationService Loc
|
||||
|
||||
@*
|
||||
Counterpart of the "EnvelopeNotFound" view (rendered by
|
||||
EnvelopeGenerator.Web.Extensions.ViewExtensions.ViewEnvelopeNotFound()).
|
||||
*@
|
||||
|
||||
<PageTitle>@Loc["EnvelopeNotFoundTitle"]</PageTitle>
|
||||
|
||||
<div class="page container p-5">
|
||||
<header class="text-center">
|
||||
<h1>@Loc["EnvelopeNotFoundTitle"]</h1>
|
||||
</header>
|
||||
<section class="text-center">
|
||||
<p>@Loc["EnvelopeNotFoundBody"]</p>
|
||||
</section>
|
||||
</div>
|
||||
|
||||
@code {
|
||||
protected override async Task OnInitializedAsync() => await Loc.EnsureLoadedAsync();
|
||||
}
|
||||
@@ -1,110 +0,0 @@
|
||||
@page "/envelope/{Key}"
|
||||
@layout EnvelopeGenerator.ReceiverUI.Web.Client.Layout.ReceiverLayout
|
||||
@rendermode InteractiveAuto
|
||||
@inject ReceiverApiClient Api
|
||||
@inject ReceiverAuthState State
|
||||
@inject LocalizationService Loc
|
||||
@inject NavigationManager Nav
|
||||
|
||||
@*
|
||||
Counterpart of EnvelopeGenerator.Web/Controllers/EnvelopeController.Main.
|
||||
|
||||
Behavior:
|
||||
1. Calls GET /api/receiverauth/{key}/status.
|
||||
2. Routes to a sub-view based on the response Status:
|
||||
- requires_access_code / requires_tfa ? EnvelopeLockedView (Phase 3)
|
||||
- show_document ? ShowEnvelopeView (Phase 4)
|
||||
- already_signed ? /envelope-signed
|
||||
- rejected ? /envelope-rejected
|
||||
- not_found ? /envelope-not-found
|
||||
- expired ? /envelope-expired
|
||||
- error ? inline error banner
|
||||
|
||||
Sub-views are simple placeholders here; they are filled with real UI
|
||||
in later phases. The routing skeleton just needs to compile and
|
||||
transition correctly.
|
||||
*@
|
||||
|
||||
<PageTitle>@(Auth?.Title ?? Loc["SignDoc"])</PageTitle>
|
||||
|
||||
@if (_loading)
|
||||
{
|
||||
<div class="page container p-5 text-center">
|
||||
<DxLoadingPanel Visible="true" IsContentBlocked="false" ApplyBackgroundShading="false" />
|
||||
</div>
|
||||
}
|
||||
else if (Auth is null)
|
||||
{
|
||||
<div class="page container p-5 text-center">
|
||||
<p class="alert alert-danger">@Loc["UnexpectedErrorTitle"]</p>
|
||||
</div>
|
||||
}
|
||||
else
|
||||
{
|
||||
switch (Auth.Status)
|
||||
{
|
||||
case ReceiverAuthStatus.RequiresAccessCode:
|
||||
case ReceiverAuthStatus.RequiresTfa:
|
||||
<EnvelopeLockedView EnvelopeKey="@Key" />
|
||||
break;
|
||||
|
||||
case ReceiverAuthStatus.ShowDocument:
|
||||
<ShowEnvelopeView EnvelopeKey="@Key" />
|
||||
break;
|
||||
|
||||
default:
|
||||
<div class="page container p-5 text-center">
|
||||
<p class="alert alert-warning">@(Auth.ErrorMessage ?? Loc["UnexpectedErrorTitle"])</p>
|
||||
</div>
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
@code {
|
||||
[Parameter] public string Key { get; set; } = string.Empty;
|
||||
|
||||
private bool _loading = true;
|
||||
private ReceiverAuthResponse? Auth => State.Current;
|
||||
|
||||
protected override async Task OnInitializedAsync()
|
||||
{
|
||||
await Loc.EnsureLoadedAsync();
|
||||
State.Changed += OnStateChanged;
|
||||
}
|
||||
|
||||
protected override async Task OnParametersSetAsync()
|
||||
{
|
||||
// Re-fetch status if the route key changed or no response loaded yet.
|
||||
if (State.EnvelopeKey != Key || State.Current is null)
|
||||
{
|
||||
_loading = true;
|
||||
var res = await Api.GetStatusAsync(Key);
|
||||
State.Set(Key, res);
|
||||
RedirectIfTerminal(res);
|
||||
_loading = false;
|
||||
}
|
||||
else
|
||||
{
|
||||
_loading = false;
|
||||
}
|
||||
}
|
||||
|
||||
private void RedirectIfTerminal(ReceiverAuthResponse? res)
|
||||
{
|
||||
if (res is null) return;
|
||||
var target = res.Status switch
|
||||
{
|
||||
ReceiverAuthStatus.AlreadySigned => "/envelope-signed",
|
||||
ReceiverAuthStatus.Rejected => "/envelope-rejected",
|
||||
ReceiverAuthStatus.NotFound => "/envelope-not-found",
|
||||
ReceiverAuthStatus.Expired => "/envelope-expired",
|
||||
_ => null
|
||||
};
|
||||
if (target is not null)
|
||||
Nav.NavigateTo(target, replace: true);
|
||||
}
|
||||
|
||||
private void OnStateChanged() => InvokeAsync(StateHasChanged);
|
||||
|
||||
public void Dispose() => State.Changed -= OnStateChanged;
|
||||
}
|
||||
@@ -1,39 +0,0 @@
|
||||
@page "/envelope-rejected"
|
||||
@layout EnvelopeGenerator.ReceiverUI.Web.Client.Layout.ReceiverLayout
|
||||
@rendermode InteractiveAuto
|
||||
@inject ReceiverAuthState State
|
||||
@inject LocalizationService Loc
|
||||
|
||||
@*
|
||||
Counterpart of Views/Envelope/EnvelopeRejected.cshtml.
|
||||
Reads envelope title / sender info from the cached auth response,
|
||||
which is populated by EnvelopePage before navigation occurs.
|
||||
*@
|
||||
|
||||
<PageTitle>@Loc["DocRejected"]</PageTitle>
|
||||
|
||||
<div class="page container p-5">
|
||||
<header class="text-center">
|
||||
<div class="icon rejected"></div>
|
||||
<h1>@Loc["RejectionInfo1"]</h1>
|
||||
</header>
|
||||
<section class="text-center">
|
||||
<small class="text-body-secondary">
|
||||
@(Loc["RejectionInfo2"])
|
||||
</small>
|
||||
@if (State.Current is not null)
|
||||
{
|
||||
<p class="mt-3">
|
||||
<strong>@State.Current.Title</strong>
|
||||
@if (!string.IsNullOrEmpty(State.Current.SenderEmail))
|
||||
{
|
||||
<span> — <a href="mailto:@State.Current.SenderEmail">@State.Current.SenderEmail</a></span>
|
||||
}
|
||||
</p>
|
||||
}
|
||||
</section>
|
||||
</div>
|
||||
|
||||
@code {
|
||||
protected override async Task OnInitializedAsync() => await Loc.EnsureLoadedAsync();
|
||||
}
|
||||
@@ -1,31 +0,0 @@
|
||||
@page "/envelope-signed"
|
||||
@layout EnvelopeGenerator.ReceiverUI.Web.Client.Layout.ReceiverLayout
|
||||
@rendermode InteractiveAuto
|
||||
@inject LocalizationService Loc
|
||||
|
||||
@*
|
||||
Counterpart of Views/Envelope/EnvelopeSigned.cshtml.
|
||||
Full styling (icon + section card) is migrated in Phase 6.
|
||||
*@
|
||||
|
||||
<PageTitle>@Loc["DocumentSuccessfullySigned"]</PageTitle>
|
||||
|
||||
<div class="page container p-5">
|
||||
<header class="text-center">
|
||||
<div class="icon signed">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="72" height="72" fill="currentColor"
|
||||
class="bi bi-check2-circle" viewBox="0 0 16 16">
|
||||
<path d="M2.5 8a5.5 5.5 0 0 1 8.25-4.764.5.5 0 0 0 .5-.866A6.5 6.5 0 1 0 14.5 8a.5.5 0 0 0-1 0 5.5 5.5 0 1 1-11 0z" />
|
||||
<path d="M15.354 3.354a.5.5 0 0 0-.708-.708L8 9.293 5.354 6.646a.5.5 0 1 0-.708.708l3 3a.5.5 0 0 0 .708 0l7-7z" />
|
||||
</svg>
|
||||
</div>
|
||||
<h1>@Loc["DocumentSuccessfullySigned"]</h1>
|
||||
</header>
|
||||
<section class="text-center">
|
||||
<p>@Loc["DocumentSignedConfirmationMessage"]</p>
|
||||
</section>
|
||||
</div>
|
||||
|
||||
@code {
|
||||
protected override async Task OnInitializedAsync() => await Loc.EnsureLoadedAsync();
|
||||
}
|
||||
@@ -1,88 +0,0 @@
|
||||
@page "/error404"
|
||||
@layout EnvelopeGenerator.ReceiverUI.Web.Client.Layout.ReceiverLayout
|
||||
@rendermode InteractiveAuto
|
||||
@inject LocalizationService Loc
|
||||
@inject NavigationManager Nav
|
||||
@inject IJSRuntime JS
|
||||
|
||||
@*
|
||||
Counterpart of HomeController.Error404 ? Views/Shared/_Error.cshtml.
|
||||
|
||||
The legacy view fully replaces the document with a black-space themed
|
||||
layout. In Blazor we keep the receiver layout intact (so the user can
|
||||
still reach the language switcher) and only scope error-space.css to
|
||||
this page via <HeadContent>. JS animation (visor + cord) is initialized
|
||||
once after the canvas elements are in the DOM.
|
||||
*@
|
||||
|
||||
<HeadContent>
|
||||
<link rel="stylesheet" href="/css/error-space.css" />
|
||||
</HeadContent>
|
||||
|
||||
<PageTitle>404</PageTitle>
|
||||
|
||||
<div class="error-space-stage" style="position:relative; height:80vh; overflow:hidden">
|
||||
<div class="moon"></div>
|
||||
<div class="moon__crater moon__crater1"></div>
|
||||
<div class="moon__crater moon__crater2"></div>
|
||||
<div class="moon__crater moon__crater3"></div>
|
||||
|
||||
<div class="star star1"></div>
|
||||
<div class="star star2"></div>
|
||||
<div class="star star3"></div>
|
||||
<div class="star star4"></div>
|
||||
<div class="star star5"></div>
|
||||
|
||||
<div class="error">
|
||||
<div class="error__title">404</div>
|
||||
<div class="error__subtitle">@Loc["PageNotFound"]</div>
|
||||
<div class="error__description">@Loc["PageNotFoundDescription"]</div>
|
||||
<a href="/" class="error__button error__button--active">@Loc["Home"]</a>
|
||||
</div>
|
||||
|
||||
<div class="astronaut">
|
||||
<div class="astronaut__backpack"></div>
|
||||
<div class="astronaut__body"></div>
|
||||
<div class="astronaut__body__chest"></div>
|
||||
<div class="astronaut__arm-left1"></div>
|
||||
<div class="astronaut__arm-left2"></div>
|
||||
<div class="astronaut__arm-right1"></div>
|
||||
<div class="astronaut__arm-right2"></div>
|
||||
<div class="astronaut__arm-thumb-left"></div>
|
||||
<div class="astronaut__arm-thumb-right"></div>
|
||||
<div class="astronaut__leg-left"></div>
|
||||
<div class="astronaut__leg-right"></div>
|
||||
<div class="astronaut__foot-left"></div>
|
||||
<div class="astronaut__foot-right"></div>
|
||||
<div class="astronaut__wrist-left"></div>
|
||||
<div class="astronaut__wrist-right"></div>
|
||||
|
||||
<div class="astronaut__cord">
|
||||
<canvas id="cord" height="500" width="500"></canvas>
|
||||
</div>
|
||||
|
||||
<div class="astronaut__head">
|
||||
<canvas id="visor" width="60" height="60"></canvas>
|
||||
<div class="astronaut__head-visor-flare1"></div>
|
||||
<div class="astronaut__head-visor-flare2"></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@code {
|
||||
protected override async Task OnInitializedAsync() => await Loc.EnsureLoadedAsync();
|
||||
|
||||
protected override async Task OnAfterRenderAsync(bool firstRender)
|
||||
{
|
||||
if (!firstRender) return;
|
||||
try
|
||||
{
|
||||
const string js = "if (!window.__errSpaceLoaded) { window.__errSpaceLoaded = true; var s = document.createElement('script'); s.src = '/js/error-space.js'; document.body.appendChild(s); }";
|
||||
await JS.InvokeVoidAsync("eval", js);
|
||||
}
|
||||
catch
|
||||
{
|
||||
// Animation is purely decorative — failing to load it is fine.
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,42 +0,0 @@
|
||||
@page "/"
|
||||
@layout EnvelopeGenerator.ReceiverUI.Web.Client.Layout.ReceiverLayout
|
||||
@rendermode InteractiveAuto
|
||||
@inject LocalizationService Loc
|
||||
|
||||
<PageTitle>@Loc["Home"]</PageTitle>
|
||||
|
||||
@*
|
||||
Counterpart of EnvelopeGenerator.Web/Views/Home/Main.cshtml.
|
||||
|
||||
The legacy view animates the description with typed.js. The Blazor
|
||||
version omits the typewriter effect because it adds another JS
|
||||
dependency for marginal value; the static description is shown
|
||||
instead. Custom company / app logos are loaded from /img/ if
|
||||
available, otherwise gracefully hidden via onerror.
|
||||
*@
|
||||
|
||||
<div class="page container py-4 px-4">
|
||||
<header class="text-center">
|
||||
<div class="header-1 alert alert-secondary" role="alert">
|
||||
<h3 class="text">@Loc["WelcomeToTheESignPortal"]</h3>
|
||||
<img class="dd-locked-logo" src="/img/company.svg"
|
||||
onerror="this.style.display='none'" alt="" />
|
||||
</div>
|
||||
<div class="icon mt-4 mb-1">
|
||||
<img class="signFlow-logo" src="/img/sign_flow_horizontal.svg"
|
||||
onerror="this.style.display='none'" alt="signFLOW" />
|
||||
</div>
|
||||
</header>
|
||||
<section class="text-center">
|
||||
<div class="alert alert-light" role="alert">
|
||||
<p class="home-description">@Loc["HomePageDescription"]</p>
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||
|
||||
@code {
|
||||
protected override async Task OnInitializedAsync()
|
||||
{
|
||||
await Loc.EnsureLoadedAsync();
|
||||
}
|
||||
}
|
||||
@@ -1,461 +0,0 @@
|
||||
@implements IDisposable
|
||||
@inject ReceiverApiClient Api
|
||||
@inject ReceiverAuthState State
|
||||
@inject LocalizationService Loc
|
||||
@inject NavigationManager Nav
|
||||
@inject ILogger<ShowEnvelopeView> Logger
|
||||
|
||||
@*
|
||||
Counterpart of EnvelopeGenerator.Web/Views/Envelope/ShowEnvelope.cshtml.
|
||||
|
||||
Sign flow (Phase 5):
|
||||
• Document is rendered by DxPdfViewer for review.
|
||||
• A side panel lists every signature placeholder the receiver has
|
||||
to sign (GET /api/annotation/elements). Each entry opens
|
||||
SignaturePadDialog to capture the signature image (+ optional
|
||||
position / city) and stores the result locally.
|
||||
• Complete validates that every placeholder is signed, then submits
|
||||
the BlazorSignaturePayload (POST /api/annotation/blazor) and
|
||||
navigates to /envelope-signed.
|
||||
• Reset clears every captured signature locally (no server call).
|
||||
• Reject and read-only share popups behave as in Phase 4.
|
||||
|
||||
Why a side-panel signing UX instead of overlaying widgets on the PDF?
|
||||
• DevExpress DxPdfViewer does not expose a public surface for
|
||||
programmatic widget annotations the way PSPDFKit did.
|
||||
• A side panel is fully keyboard / screen-reader accessible, works
|
||||
identically on mobile, and avoids fragile coordinate math against
|
||||
DevExpress' internal DOM. The visual position on the PDF is still
|
||||
communicated via the "Page P" badge per entry.
|
||||
*@
|
||||
|
||||
<div class="envelope-view">
|
||||
|
||||
@* — Top toolbar / action buttons (desktop) — *@
|
||||
@if (!IsReadOnly)
|
||||
{
|
||||
<div id="flex-action-panel" class="btn-group btn_group position-fixed bottom-0 end-0 d-flex align-items-center"
|
||||
role="group">
|
||||
<DxButton CssClass="btn_complete btn btn-success btn-desktop"
|
||||
Text="@Loc["Complete"]"
|
||||
Click="OnCompleteClick"
|
||||
Enabled="@(!_busy)" />
|
||||
<DxButton CssClass="btn_reject btn btn-danger btn-desktop"
|
||||
Text="@Loc["Reject"]"
|
||||
Click="OnRejectClick"
|
||||
Enabled="@(!_busy)" />
|
||||
<DxButton CssClass="btn_refresh btn btn-secondary btn-desktop"
|
||||
IconCssClass="bi bi-arrow-counterclockwise"
|
||||
Text="@Loc["Reset"]"
|
||||
Click="OnResetClick"
|
||||
Enabled="@(!_busy && _captured.Count > 0)" />
|
||||
</div>
|
||||
}
|
||||
|
||||
@* — Envelope info card — *@
|
||||
<div class="dd-cards-container">
|
||||
<div class="dd-card">
|
||||
<div class="dd-card-preview">
|
||||
<img src="/img/sign_flow_horizontal.svg" class="app-logo"
|
||||
onerror="this.style.display='none'" alt="signFLOW" />
|
||||
@if (!IsReadOnly)
|
||||
{
|
||||
<div class="progress-container">
|
||||
<div id="signed-count-bar" class="progress"></div>
|
||||
<span class="progress-text">
|
||||
<span id="signed-count">@SignedCount</span>/<span id="signature-count">@_elements.Count</span>
|
||||
@Loc["Signatures"]
|
||||
</span>
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
<div class="dd-card-info">
|
||||
<h2>@(State.Current?.Title)</h2>
|
||||
@if (!string.IsNullOrEmpty(State.Current?.Message))
|
||||
{
|
||||
<div class="markdown">@State.Current.Message</div>
|
||||
}
|
||||
@if (!string.IsNullOrEmpty(State.Current?.SenderEmail))
|
||||
{
|
||||
<p>
|
||||
<small class="text-body-secondary">
|
||||
<a class="mail-link" href="mailto:@State.Current.SenderEmail">
|
||||
@State.Current.SenderEmail
|
||||
</a>
|
||||
</small>
|
||||
</p>
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row g-3 m-0 envelope-content">
|
||||
|
||||
@* — PDF viewer — *@
|
||||
<div class="col-12 col-lg-8 p-0">
|
||||
<div id="pdfviewer-host" style="min-height:60vh; height:70vh;">
|
||||
@if (_loadingDoc)
|
||||
{
|
||||
<div class="d-flex align-items-center justify-content-center h-100">
|
||||
<DxLoadingPanel Visible="true" IsContentBlocked="false" ApplyBackgroundShading="false" />
|
||||
</div>
|
||||
}
|
||||
else if (_documentBytes is { Length: > 0 })
|
||||
{
|
||||
<DxPdfViewer DocumentContent="_documentBytes"
|
||||
CssClass="h-100 w-100"
|
||||
ZoomLevel="1" />
|
||||
}
|
||||
else
|
||||
{
|
||||
<div class="alert alert-warning m-3" role="alert">
|
||||
@Loc["DocumentNotFound"]
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@* — Side panel: signature placeholders to sign — *@
|
||||
@if (!IsReadOnly)
|
||||
{
|
||||
<aside class="col-12 col-lg-4 p-3">
|
||||
<h5>@Loc["SignaturePlaceholders"]</h5>
|
||||
@if (_loadingElements)
|
||||
{
|
||||
<DxLoadingPanel Visible="true" IsContentBlocked="false" ApplyBackgroundShading="false" />
|
||||
}
|
||||
else if (_elements.Count == 0)
|
||||
{
|
||||
<p class="text-body-secondary">@Loc["NoSignatureRequired"]</p>
|
||||
}
|
||||
else
|
||||
{
|
||||
<ol class="signature-list list-unstyled">
|
||||
@foreach (var (el, idx) in _elements.Select((e, i) => (e, i + 1)))
|
||||
{
|
||||
var signed = _captured.ContainsKey(el.Id);
|
||||
<li class="signature-item d-flex align-items-center justify-content-between gap-2 p-2 border rounded mb-2 @(signed ? "bg-success-subtle" : "")">
|
||||
<div>
|
||||
<strong>#@idx</strong>
|
||||
<span class="badge bg-secondary ms-1">@Loc["Page"] @el.Page</span>
|
||||
@if (!string.IsNullOrEmpty(el.Tooltip))
|
||||
{
|
||||
<div class="small text-body-secondary">@el.Tooltip</div>
|
||||
}
|
||||
@if (signed)
|
||||
{
|
||||
<div class="small text-success">
|
||||
<i class="bi bi-check2-circle"></i> @Loc["Signed"]
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
<DxButton Text="@(signed ? Loc["Change"] : Loc["Sign"])"
|
||||
RenderStyle="@(signed ? ButtonRenderStyle.Secondary : ButtonRenderStyle.Primary)"
|
||||
IconCssClass="bi bi-pen"
|
||||
Click="@(() => OpenPadAsync(el))" />
|
||||
</li>
|
||||
}
|
||||
</ol>
|
||||
}
|
||||
</aside>
|
||||
}
|
||||
</div>
|
||||
|
||||
@* — Signature pad dialog — *@
|
||||
<SignaturePadDialog @ref="_padDialog" Confirmed="OnSignatureConfirmed" />
|
||||
|
||||
@* — Confirm-complete popup — *@
|
||||
<DxPopup @bind-Visible="_confirmCompleteVisible"
|
||||
HeaderText="@Loc["ConfirmSigning"]"
|
||||
ShowCloseButton="true"
|
||||
Width="32rem">
|
||||
<BodyContentTemplate Context="confirmCtx">
|
||||
<p>@Loc["ConfirmSigningQ"]</p>
|
||||
</BodyContentTemplate>
|
||||
<FooterContentTemplate Context="confirmFootCtx">
|
||||
<DxButton Text="@Loc["Confirm"]"
|
||||
RenderStyle="ButtonRenderStyle.Primary"
|
||||
Click="OnCompleteSubmit" Enabled="@(!_busy)" />
|
||||
<DxButton Text="@Loc["Cancel"]"
|
||||
RenderStyle="ButtonRenderStyle.Secondary"
|
||||
Click="@(() => _confirmCompleteVisible = false)" />
|
||||
</FooterContentTemplate>
|
||||
</DxPopup>
|
||||
|
||||
@* — Read-only share popup — *@
|
||||
@if (!IsReadOnly)
|
||||
{
|
||||
<DxPopup @bind-Visible="_shareVisible"
|
||||
HeaderText="@Loc["EnterRecipientToShareDocument"]"
|
||||
ShowCloseButton="true"
|
||||
Width="32rem">
|
||||
<BodyContentTemplate Context="shareCtx">
|
||||
<DxFormLayout>
|
||||
<DxFormLayoutItem Caption="@Loc["Email"]">
|
||||
<DxTextBox @bind-Text="_shareEmail" NullText="user@mail.com" />
|
||||
</DxFormLayoutItem>
|
||||
<DxFormLayoutItem Caption="@Loc["ValidUntil"]">
|
||||
<DxDateEdit @bind-Date="_shareValidUntil"
|
||||
MinDate="DateTime.Today.AddDays(1)"
|
||||
MaxDate="DateTime.Today.AddDays(90)" />
|
||||
</DxFormLayoutItem>
|
||||
</DxFormLayout>
|
||||
@if (!string.IsNullOrEmpty(_shareError))
|
||||
{
|
||||
<div class="alert alert-danger mt-2">@_shareError</div>
|
||||
}
|
||||
</BodyContentTemplate>
|
||||
<FooterContentTemplate Context="shareFootCtx">
|
||||
<DxButton Text="@Loc["Send"]" RenderStyle="ButtonRenderStyle.Primary"
|
||||
IconCssClass="bi bi-send" Click="OnShareSubmit"
|
||||
Enabled="@(!_busy)" />
|
||||
</FooterContentTemplate>
|
||||
</DxPopup>
|
||||
|
||||
@* — Reject popup — *@
|
||||
<DxPopup @bind-Visible="_rejectVisible"
|
||||
HeaderText="@Loc["Rejection"]"
|
||||
ShowCloseButton="true"
|
||||
Width="32rem">
|
||||
<BodyContentTemplate Context="rejectCtx">
|
||||
<p>@Loc["RejectionReasonQ"]</p>
|
||||
<DxMemo @bind-Text="_rejectReason" Rows="4" />
|
||||
</BodyContentTemplate>
|
||||
<FooterContentTemplate Context="rejectFootCtx">
|
||||
<DxButton Text="@Loc["Complete"]"
|
||||
RenderStyle="ButtonRenderStyle.Primary"
|
||||
Click="OnRejectSubmit" Enabled="@(!_busy)" />
|
||||
<DxButton Text="@Loc["Back"]"
|
||||
RenderStyle="ButtonRenderStyle.Secondary"
|
||||
Click="@(() => _rejectVisible = false)" />
|
||||
</FooterContentTemplate>
|
||||
</DxPopup>
|
||||
}
|
||||
|
||||
@if (!string.IsNullOrEmpty(_globalError))
|
||||
{
|
||||
<div class="alert alert-danger m-3" role="alert">@_globalError</div>
|
||||
}
|
||||
</div>
|
||||
|
||||
@code {
|
||||
[Parameter] public string EnvelopeKey { get; set; } = string.Empty;
|
||||
|
||||
private byte[]? _documentBytes;
|
||||
private bool _loadingDoc = true;
|
||||
private bool _loadingElements = true;
|
||||
private bool _busy;
|
||||
|
||||
private List<SignatureElementDto> _elements = new();
|
||||
private readonly Dictionary<int, BlazorSignatureEntry> _captured = new();
|
||||
|
||||
private SignaturePadDialog? _padDialog;
|
||||
private bool _confirmCompleteVisible;
|
||||
private string? _globalError;
|
||||
|
||||
private bool _shareVisible;
|
||||
private string _shareEmail = string.Empty;
|
||||
private DateTime _shareValidUntil = DateTime.Today.AddDays(7);
|
||||
private string? _shareError;
|
||||
|
||||
private bool _rejectVisible;
|
||||
private string _rejectReason = string.Empty;
|
||||
|
||||
private bool IsReadOnly => State.Current?.ReadOnly ?? false;
|
||||
private int SignedCount => _captured.Count;
|
||||
|
||||
protected override async Task OnInitializedAsync()
|
||||
{
|
||||
await Loc.EnsureLoadedAsync();
|
||||
State.Changed += OnStateChanged;
|
||||
}
|
||||
|
||||
protected override async Task OnParametersSetAsync()
|
||||
{
|
||||
if (_documentBytes is null && !string.IsNullOrEmpty(EnvelopeKey))
|
||||
{
|
||||
await LoadDocumentAsync();
|
||||
if (!IsReadOnly)
|
||||
await LoadElementsAsync();
|
||||
}
|
||||
}
|
||||
|
||||
private async Task LoadDocumentAsync()
|
||||
{
|
||||
_loadingDoc = true;
|
||||
try
|
||||
{
|
||||
_documentBytes = await Api.GetDocumentAsync(EnvelopeKey);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Logger.LogError(ex, "Failed to load document for key {Key}", EnvelopeKey);
|
||||
}
|
||||
finally
|
||||
{
|
||||
_loadingDoc = false;
|
||||
}
|
||||
}
|
||||
|
||||
private async Task LoadElementsAsync()
|
||||
{
|
||||
_loadingElements = true;
|
||||
try
|
||||
{
|
||||
_elements = await Api.GetSignatureElementsAsync();
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Logger.LogError(ex, "Failed to load signature elements.");
|
||||
_elements = new();
|
||||
}
|
||||
finally
|
||||
{
|
||||
_loadingElements = false;
|
||||
}
|
||||
}
|
||||
|
||||
private void OnStateChanged() => InvokeAsync(StateHasChanged);
|
||||
|
||||
// — Signature pad ?????????????????????????????????????????????????
|
||||
|
||||
private async Task OpenPadAsync(SignatureElementDto el)
|
||||
{
|
||||
if (_padDialog is null) return;
|
||||
_captured.TryGetValue(el.Id, out var existing);
|
||||
await _padDialog.ShowAsync(el.Id, existing?.Position, existing?.City);
|
||||
}
|
||||
|
||||
private void OnSignatureConfirmed(BlazorSignatureEntry entry)
|
||||
{
|
||||
_captured[entry.ElementId] = entry;
|
||||
StateHasChanged();
|
||||
}
|
||||
|
||||
// — Toolbar actions ????????????????????????????????????????????????
|
||||
|
||||
private Task OnCompleteClick()
|
||||
{
|
||||
_globalError = null;
|
||||
var missing = _elements.Where(e => !_captured.ContainsKey(e.Id)).ToList();
|
||||
if (missing.Count > 0)
|
||||
{
|
||||
_globalError = Loc.Format("MissingSignaturesFmt", missing.Count);
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
_confirmCompleteVisible = true;
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
private async Task OnCompleteSubmit()
|
||||
{
|
||||
if (_busy) return;
|
||||
_busy = true;
|
||||
try
|
||||
{
|
||||
var payload = new BlazorSignaturePayload
|
||||
{
|
||||
Signatures = _captured.Values.ToList()
|
||||
};
|
||||
var status = await Api.SignBlazorAsync(payload);
|
||||
_confirmCompleteVisible = false;
|
||||
if ((int)status >= 200 && (int)status < 300)
|
||||
{
|
||||
Nav.NavigateTo("/envelope-signed", replace: true);
|
||||
}
|
||||
else
|
||||
{
|
||||
_globalError = $"{Loc["UnexpectedErrorTitle"]} ({(int)status})";
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Logger.LogError(ex, "Sign submit failed.");
|
||||
_globalError = Loc["UnexpectedErrorTitle"];
|
||||
}
|
||||
finally
|
||||
{
|
||||
_busy = false;
|
||||
}
|
||||
}
|
||||
|
||||
private Task OnRejectClick()
|
||||
{
|
||||
_rejectReason = string.Empty;
|
||||
_rejectVisible = true;
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
private async Task OnRejectSubmit()
|
||||
{
|
||||
if (_busy) return;
|
||||
_busy = true;
|
||||
try
|
||||
{
|
||||
var ok = await Api.RejectAsync(_rejectReason ?? string.Empty);
|
||||
_rejectVisible = false;
|
||||
if (ok)
|
||||
Nav.NavigateTo("/envelope-rejected", replace: true);
|
||||
}
|
||||
finally
|
||||
{
|
||||
_busy = false;
|
||||
}
|
||||
}
|
||||
|
||||
private void OnResetClick()
|
||||
{
|
||||
_captured.Clear();
|
||||
_globalError = null;
|
||||
}
|
||||
|
||||
// — Share read-only ?????????????????????????????????????????????????
|
||||
|
||||
private async Task OnShareSubmit()
|
||||
{
|
||||
_shareError = null;
|
||||
if (string.IsNullOrWhiteSpace(_shareEmail) ||
|
||||
!System.Text.RegularExpressions.Regex.IsMatch(_shareEmail, @"^\S+@\S+\.\S+$"))
|
||||
{
|
||||
_shareError = Loc["ShrEnvInvalidEmailText"];
|
||||
return;
|
||||
}
|
||||
if (_shareValidUntil < DateTime.Today.AddDays(1))
|
||||
{
|
||||
_shareError = Loc["ShrEnvInvalidDateText"];
|
||||
return;
|
||||
}
|
||||
|
||||
_busy = true;
|
||||
try
|
||||
{
|
||||
var ok = await Api.ShareReadOnlyAsync(new ReadOnlyShareRequest
|
||||
{
|
||||
ReceiverMail = _shareEmail,
|
||||
DateValid = _shareValidUntil
|
||||
});
|
||||
if (ok)
|
||||
{
|
||||
_shareVisible = false;
|
||||
_shareEmail = string.Empty;
|
||||
_shareValidUntil = DateTime.Today.AddDays(7);
|
||||
}
|
||||
else
|
||||
{
|
||||
_shareError = Loc["ShrEnvOperationFailedText"];
|
||||
}
|
||||
}
|
||||
catch
|
||||
{
|
||||
_shareError = Loc["UnexpectedErrorTitle"];
|
||||
}
|
||||
finally
|
||||
{
|
||||
_busy = false;
|
||||
}
|
||||
}
|
||||
|
||||
public void Dispose() => State.Changed -= OnStateChanged;
|
||||
}
|
||||
|
||||
|
||||
@@ -1,150 +0,0 @@
|
||||
@implements IAsyncDisposable
|
||||
@inject IJSRuntime JS
|
||||
@inject LocalizationService Loc
|
||||
|
||||
@*
|
||||
Modal dialog that captures one signature for a single placeholder.
|
||||
|
||||
The user is asked to:
|
||||
• draw their signature (mouse / touch),
|
||||
• optionally fill in "position" (job title) and "city",
|
||||
• confirm — which produces a BlazorSignatureEntry and closes the dialog.
|
||||
|
||||
The drawing surface is a plain HTML5 canvas wired up by signature-pad.js
|
||||
(loaded once in App.razor). All JS interop is encapsulated here so the
|
||||
rest of the receiver UI is free of DOM concerns.
|
||||
*@
|
||||
|
||||
<DxPopup @bind-Visible="_visible"
|
||||
HeaderText="@Loc["YourSignature"]"
|
||||
ShowCloseButton="true"
|
||||
CloseOnEscape="true"
|
||||
Width="36rem"
|
||||
Closed="OnClosed">
|
||||
<BodyContentTemplate Context="padCtx">
|
||||
<div class="signature-pad-container">
|
||||
<canvas id="@_canvasId"
|
||||
class="signature-pad-canvas"
|
||||
style="width:100%; height:200px; border:1px solid #cfd6dd; border-radius:.25rem; background:#fff; touch-action:none"></canvas>
|
||||
|
||||
<div class="d-flex gap-2 mt-2">
|
||||
<DxButton Text="@Loc["Clear"]"
|
||||
RenderStyle="ButtonRenderStyle.Secondary"
|
||||
IconCssClass="bi bi-eraser"
|
||||
Click="ClearAsync" />
|
||||
</div>
|
||||
|
||||
<div class="mt-3">
|
||||
<DxFormLayout>
|
||||
<DxFormLayoutItem Caption="@Loc["Position"]">
|
||||
<DxTextBox @bind-Text="_position" NullText="@Loc["PositionPlaceholder"]" />
|
||||
</DxFormLayoutItem>
|
||||
<DxFormLayoutItem Caption="@Loc["City"]">
|
||||
<DxTextBox @bind-Text="_city" NullText="@Loc["CityPlaceholder"]" />
|
||||
</DxFormLayoutItem>
|
||||
</DxFormLayout>
|
||||
</div>
|
||||
|
||||
@if (!string.IsNullOrEmpty(_error))
|
||||
{
|
||||
<div class="alert alert-danger mt-2 mb-0">@_error</div>
|
||||
}
|
||||
</div>
|
||||
</BodyContentTemplate>
|
||||
<FooterContentTemplate Context="footCtx">
|
||||
<DxButton Text="@Loc["Confirm"]"
|
||||
RenderStyle="ButtonRenderStyle.Primary"
|
||||
IconCssClass="bi bi-check2"
|
||||
Click="ConfirmAsync" />
|
||||
<DxButton Text="@Loc["Cancel"]"
|
||||
RenderStyle="ButtonRenderStyle.Secondary"
|
||||
Click="@(() => Hide())" />
|
||||
</FooterContentTemplate>
|
||||
</DxPopup>
|
||||
|
||||
@code {
|
||||
/// <summary>Fired when the user confirms a valid signature.</summary>
|
||||
[Parameter] public EventCallback<BlazorSignatureEntry> Confirmed { get; set; }
|
||||
|
||||
private readonly string _canvasId = $"sigpad_{Guid.NewGuid():N}";
|
||||
private bool _visible;
|
||||
private bool _attached;
|
||||
private int _elementId;
|
||||
private string _position = string.Empty;
|
||||
private string _city = string.Empty;
|
||||
private string? _error;
|
||||
|
||||
/// <summary>Opens the dialog and binds JS interop on the canvas.</summary>
|
||||
public async Task ShowAsync(int elementId, string? defaultPosition = null, string? defaultCity = null)
|
||||
{
|
||||
_elementId = elementId;
|
||||
_position = defaultPosition ?? string.Empty;
|
||||
_city = defaultCity ?? string.Empty;
|
||||
_error = null;
|
||||
_visible = true;
|
||||
StateHasChanged();
|
||||
|
||||
// The canvas only exists after the popup is rendered. Wait one
|
||||
// render cycle, then attach the pad.
|
||||
await Task.Yield();
|
||||
try
|
||||
{
|
||||
_attached = await JS.InvokeAsync<bool>("signaturePad.attach", _canvasId);
|
||||
}
|
||||
catch (JSException ex)
|
||||
{
|
||||
_error = ex.Message;
|
||||
_attached = false;
|
||||
}
|
||||
}
|
||||
|
||||
public void Hide()
|
||||
{
|
||||
_visible = false;
|
||||
}
|
||||
|
||||
private async Task ClearAsync()
|
||||
{
|
||||
if (_attached)
|
||||
await JS.InvokeVoidAsync("signaturePad.clear", _canvasId);
|
||||
}
|
||||
|
||||
private async Task ConfirmAsync()
|
||||
{
|
||||
if (!_attached)
|
||||
{
|
||||
_error = Loc["SignaturePadNotReady"];
|
||||
return;
|
||||
}
|
||||
|
||||
var dataUrl = await JS.InvokeAsync<string?>("signaturePad.toDataUrl", _canvasId);
|
||||
if (string.IsNullOrEmpty(dataUrl))
|
||||
{
|
||||
_error = Loc["SignatureRequired"];
|
||||
return;
|
||||
}
|
||||
|
||||
await Confirmed.InvokeAsync(new BlazorSignatureEntry
|
||||
{
|
||||
ElementId = _elementId,
|
||||
SignatureDataUrl = dataUrl,
|
||||
Position = string.IsNullOrWhiteSpace(_position) ? null : _position.Trim(),
|
||||
City = string.IsNullOrWhiteSpace(_city) ? null : _city.Trim(),
|
||||
SignedAt = DateTime.Now,
|
||||
});
|
||||
|
||||
await OnClosed();
|
||||
_visible = false;
|
||||
}
|
||||
|
||||
private async Task OnClosed()
|
||||
{
|
||||
if (_attached)
|
||||
{
|
||||
try { await JS.InvokeVoidAsync("signaturePad.detach", _canvasId); } catch { /* ignore */ }
|
||||
_attached = false;
|
||||
}
|
||||
}
|
||||
|
||||
public async ValueTask DisposeAsync() => await OnClosed();
|
||||
}
|
||||
@@ -1,158 +0,0 @@
|
||||
@page "/tfa/{Key}"
|
||||
@layout EnvelopeGenerator.ReceiverUI.Web.Client.Layout.ReceiverLayout
|
||||
@rendermode InteractiveAuto
|
||||
@inject ReceiverApiClient Api
|
||||
@inject LocalizationService Loc
|
||||
@inject NavigationManager Nav
|
||||
|
||||
@*
|
||||
Counterpart of TFARegController.Reg ? Views/TFAReg/Reg.cshtml.
|
||||
|
||||
The legacy view uses Bootstrap's collapse-based accordion to walk the
|
||||
receiver through 3 steps:
|
||||
1. Install an authenticator app
|
||||
2. Scan the QR code
|
||||
3. Verify the generated 6-digit code
|
||||
|
||||
The Blazor port keeps the exact same step structure but uses
|
||||
DxAccordion so the visual / keyboard behavior matches the rest of
|
||||
the receiver UI. The TOTP QR and registration deadline are fetched
|
||||
from <c>GET /api/tfa/{key}</c> on first render.
|
||||
*@
|
||||
|
||||
<PageTitle>@Loc["TfaRegistration"]</PageTitle>
|
||||
|
||||
<div class="page container p-5">
|
||||
<header class="text-center">
|
||||
<div class="icon locked mt-4 mb-1">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="72" height="72" fill="currentColor"
|
||||
class="bi bi-shield-lock" viewBox="0 0 16 16">
|
||||
<path d="M5.338 1.59a61 61 0 0 0-2.837.856.48.48 0 0 0-.328.39c-.554 4.157.726 7.19 2.253 9.188a10.7 10.7 0 0 0 2.287 2.233c.346.244.652.42.893.533q.18.085.293.118a1 1 0 0 0 .101.025 1 1 0 0 0 .1-.025q.114-.034.294-.118c.24-.113.547-.29.893-.533a10.7 10.7 0 0 0 2.287-2.233c1.527-1.997 2.807-5.031 2.253-9.188a.48.48 0 0 0-.328-.39c-.651-.213-1.75-.56-2.837-.855C9.552 1.29 8.531 1.067 8 1.067c-.53 0-1.552.223-2.662.524zM5.072.56C6.157.265 7.31 0 8 0s1.843.265 2.928.56c1.11.3 2.229.655 2.887.87a1.54 1.54 0 0 1 1.044 1.262c.596 4.477-.787 7.795-2.465 9.99a11.8 11.8 0 0 1-2.517 2.453 7 7 0 0 1-1.048.625c-.28.132-.581.24-.829.24s-.548-.108-.829-.24a7 7 0 0 1-1.048-.625 11.8 11.8 0 0 1-2.517-2.453C1.928 10.487.545 7.169 1.141 2.692A1.54 1.54 0 0 1 2.185 1.43 63 63 0 0 1 5.072.56" />
|
||||
<path d="M9.5 6.5a1.5 1.5 0 0 1-1 1.415l.385 1.99a.5.5 0 0 1-.491.595h-.788a.5.5 0 0 1-.49-.595l.384-1.99a1.5 1.5 0 1 1 2-1.415" />
|
||||
</svg>
|
||||
</div>
|
||||
<h2 class="mb-0">2-Factor Authentication (2FA)</h2>
|
||||
<h2>@Loc["Registration"]</h2>
|
||||
</header>
|
||||
|
||||
@if (_loading)
|
||||
{
|
||||
<div class="text-center mt-4">
|
||||
<DxLoadingPanel Visible="true" IsContentBlocked="false" ApplyBackgroundShading="false" />
|
||||
</div>
|
||||
}
|
||||
else if (_error is not null)
|
||||
{
|
||||
<div class="alert alert-danger mt-4" role="alert">@_error</div>
|
||||
<div class="text-center mt-3">
|
||||
<DxButton Text="@Loc["Back"]"
|
||||
RenderStyle="ButtonRenderStyle.Secondary"
|
||||
Click="@(() => Nav.NavigateTo($"/envelope/{Key}"))" />
|
||||
</div>
|
||||
}
|
||||
else if (_data is not null)
|
||||
{
|
||||
<section class="text-center">
|
||||
<p class="p-0 m-0">
|
||||
@if (_data.TfaRegDeadline is DateTime dl)
|
||||
{
|
||||
@Loc.Format("PageVisibleUntil", dl.ToString("d. MMM, HH:mm", new System.Globalization.CultureInfo("de-DE")))
|
||||
}
|
||||
</p>
|
||||
</section>
|
||||
|
||||
<section class="text-start mt-4">
|
||||
<DxAccordion>
|
||||
<Items>
|
||||
<DxAccordionItem Text="@Loc["Step1Download2faApplication"]" Expanded="true">
|
||||
<ContentTemplate>
|
||||
<p class="text-wrap fw-medium">@Loc["Download2faAppInstruction"]</p>
|
||||
<p class="text-wrap fw-light">@Loc["Recommended2faApplications"]</p>
|
||||
<ul class="list-group text-start">
|
||||
<li class="list-group-item">
|
||||
<a href="https://support.google.com/accounts/answer/1066447?hl=de&co=GENIE.Platform%3DAndroid"
|
||||
target="_blank" rel="noopener" style="text-decoration:none">
|
||||
<samp>Google Authenticator</samp>
|
||||
</a>
|
||||
</li>
|
||||
<li class="list-group-item">
|
||||
<a href="https://support.microsoft.com/de-de/account-billing/microsoft-authenticator-herunterladen-351498fc-850a-45da-b7b6-27e523b8702a"
|
||||
target="_blank" rel="noopener" style="text-decoration:none">
|
||||
<samp>Microsoft Authenticator</samp>
|
||||
</a>
|
||||
</li>
|
||||
</ul>
|
||||
</ContentTemplate>
|
||||
</DxAccordionItem>
|
||||
|
||||
<DxAccordionItem Text="@Loc["Step2ScanQrCode"]">
|
||||
<ContentTemplate>
|
||||
<div class="text-center m-0 p-0">
|
||||
@if (!string.IsNullOrEmpty(_data.TotpQR64))
|
||||
{
|
||||
<img class="tfaQrCode"
|
||||
src="@($"data:image/png;base64,{_data.TotpQR64}")"
|
||||
alt="TOTP QR" />
|
||||
}
|
||||
</div>
|
||||
<p class="text-wrap fw-medium">@Loc["ScanQrCodeInstruction"]</p>
|
||||
</ContentTemplate>
|
||||
</DxAccordionItem>
|
||||
|
||||
<DxAccordionItem Text="@Loc["Step3VerifyTheCode"]">
|
||||
<ContentTemplate>
|
||||
<p class="text-wrap fw-medium">
|
||||
@Loc["VerifyCodeInstructionMain"]
|
||||
<samp>@Loc["VerifyCodeInstructionSubmit"]</samp>.
|
||||
</p>
|
||||
<div class="text-center mt-3">
|
||||
<DxButton Text="@Loc["BackToEnvelope"]"
|
||||
RenderStyle="ButtonRenderStyle.Primary"
|
||||
Click="@(() => Nav.NavigateTo($"/envelope/{Key}"))" />
|
||||
</div>
|
||||
</ContentTemplate>
|
||||
</DxAccordionItem>
|
||||
</Items>
|
||||
</DxAccordion>
|
||||
</section>
|
||||
}
|
||||
</div>
|
||||
|
||||
@code {
|
||||
[Parameter] public string Key { get; set; } = string.Empty;
|
||||
|
||||
private TfaRegistrationResponse? _data;
|
||||
private string? _error;
|
||||
private bool _loading = true;
|
||||
|
||||
protected override async Task OnInitializedAsync()
|
||||
{
|
||||
await Loc.EnsureLoadedAsync();
|
||||
|
||||
_loading = true;
|
||||
try
|
||||
{
|
||||
var (data, status) = await Api.GetTfaRegistrationAsync(Key);
|
||||
if ((int)status >= 200 && (int)status < 300 && data is not null)
|
||||
{
|
||||
_data = data;
|
||||
}
|
||||
else if ((int)status == 410)
|
||||
{
|
||||
_error = Loc["TfaRegDeadlineExpired"];
|
||||
}
|
||||
else if ((int)status == 401)
|
||||
{
|
||||
_error = Loc["UnauthorizedTfaReg"];
|
||||
}
|
||||
else
|
||||
{
|
||||
_error = Loc["UnexpectedErrorTitle"];
|
||||
}
|
||||
}
|
||||
finally
|
||||
{
|
||||
_loading = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,32 +0,0 @@
|
||||
@page "/weather"
|
||||
|
||||
<PageTitle>Weather</PageTitle>
|
||||
|
||||
<h1>Weather</h1>
|
||||
|
||||
<DxGrid Data="@forecasts">
|
||||
<Columns>
|
||||
<DxGridDataColumn Caption="Date" FieldName="Date" />
|
||||
<DxGridDataColumn Caption="Temperature (C)" FieldName="TemperatureC" />
|
||||
<DxGridDataColumn Caption="Temperature (F)" FieldName="TemperatureF" />
|
||||
<DxGridDataColumn Caption="Summary" FieldName="Summary" />
|
||||
</Columns>
|
||||
</DxGrid>
|
||||
|
||||
@code {
|
||||
private WeatherForecast[]? forecasts;
|
||||
|
||||
protected override async Task OnInitializedAsync() {
|
||||
DateOnly startDate = DateOnly.FromDateTime(DateTime.Now);
|
||||
string[] summaries = ["Freezing", "Bracing", "Chilly", "Cool", "Mild", "Warm", "Balmy", "Hot", "Sweltering", "Scorching"];
|
||||
forecasts = Enumerable.Range(1, 5).Select(index => new WeatherForecast(
|
||||
Date: startDate.AddDays(index),
|
||||
TemperatureC: Random.Shared.Next(-20, 55),
|
||||
Summary: summaries[Random.Shared.Next(summaries.Length)]
|
||||
)).ToArray();
|
||||
}
|
||||
|
||||
private record WeatherForecast(DateOnly Date, int TemperatureC, string? Summary) {
|
||||
public int TemperatureF => 32 + (int)(TemperatureC / 0.5556);
|
||||
}
|
||||
}
|
||||
@@ -1,31 +0,0 @@
|
||||
using EnvelopeGenerator.ReceiverUI.Web.Client.Api;
|
||||
using EnvelopeGenerator.ReceiverUI.Web.Client.Services;
|
||||
using EnvelopeGenerator.ReceiverUI.Web.Client.Utils;
|
||||
using Microsoft.AspNetCore.Components.WebAssembly.Hosting;
|
||||
|
||||
var builder = WebAssemblyHostBuilder.CreateDefault(args);
|
||||
|
||||
builder.Services.AddDevExpressBlazor(options =>
|
||||
{
|
||||
options.SizeMode = DevExpress.Blazor.SizeMode.Medium;
|
||||
});
|
||||
|
||||
builder.Services.AddChatClient(builder.HostEnvironment.BaseAddress + "api/chat", "proxykey", "proxychat");
|
||||
|
||||
builder.Services.AddDevExpressWebAssemblyBlazorPdfViewer();
|
||||
|
||||
DevExpress.XtraPrinting.PrintingOptions.Pdf.RenderingEngine = DevExpress.XtraPrinting.XRPdfRenderingEngine.Skia;
|
||||
|
||||
// ── Receiver API + Auth + Localization ─────────────────────────────
|
||||
// Same-origin HttpClient: the BFF (EnvelopeGenerator.ReceiverUI.Web)
|
||||
// reverse-proxies /api/** to EnvelopeGenerator.API and forwards the
|
||||
// HttpOnly authentication cookie automatically.
|
||||
builder.Services.AddHttpClient<ReceiverApiClient>(client =>
|
||||
{
|
||||
client.BaseAddress = new Uri(builder.HostEnvironment.BaseAddress);
|
||||
});
|
||||
builder.Services.AddScoped<LocalizationService>();
|
||||
builder.Services.AddScoped<ReceiverAuthState>();
|
||||
|
||||
await builder.Build().RunAsync();
|
||||
|
||||
@@ -1,14 +0,0 @@
|
||||
<Router AppAssembly="typeof(Program).Assembly">
|
||||
<Found Context="routeData">
|
||||
<RouteView RouteData="routeData" DefaultLayout="typeof(Layout.MainLayout)" />
|
||||
</Found>
|
||||
<NotFound>
|
||||
@*
|
||||
Mirrors HomeController.Error404 from EnvelopeGenerator.Web,
|
||||
which maps fallback requests to the Error404 view.
|
||||
*@
|
||||
<LayoutView Layout="@typeof(Layout.ReceiverLayout)">
|
||||
<Pages.Receiver.Error404 />
|
||||
</LayoutView>
|
||||
</NotFound>
|
||||
</Router>
|
||||
@@ -1,119 +0,0 @@
|
||||
using EnvelopeGenerator.ReceiverUI.Web.Client.Api;
|
||||
|
||||
namespace EnvelopeGenerator.ReceiverUI.Web.Client.Services;
|
||||
|
||||
/// <summary>
|
||||
/// Pulls all localized strings from the API once and exposes them
|
||||
/// via an indexer mimicking <c>IStringLocalizer<Resource>["Key"]</c>
|
||||
/// in the legacy MVC Web project.
|
||||
///
|
||||
/// Missing keys fall back to the key itself (matching the previous behavior
|
||||
/// where <c>_localizer["X"].Value</c> returned "X" if not found).
|
||||
///
|
||||
/// Components consume this service as a scoped dependency and may call
|
||||
/// <see cref="EnsureLoadedAsync"/> in <c>OnInitializedAsync</c>.
|
||||
/// </summary>
|
||||
public class LocalizationService
|
||||
{
|
||||
private readonly ReceiverApiClient _api;
|
||||
private readonly ILogger<LocalizationService> _logger;
|
||||
private Dictionary<string, string> _strings = new();
|
||||
private Task? _loadTask;
|
||||
private readonly object _gate = new();
|
||||
|
||||
public LocalizationService(ReceiverApiClient api, ILogger<LocalizationService> logger)
|
||||
{
|
||||
_api = api;
|
||||
_logger = logger;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Languages exposed in the footer's language switcher. Kept as a
|
||||
/// small in-memory list so the receiver UI does not need an extra
|
||||
/// API roundtrip just to populate a dropdown. Mirrors the cultures
|
||||
/// configured server-side (de-DE, en-US, tr-TR).
|
||||
/// </summary>
|
||||
public static readonly IReadOnlyList<(string Code, string Native, string Flag)> SupportedLanguages =
|
||||
new[]
|
||||
{
|
||||
("de", "Deutsch", "fi-de"),
|
||||
("en", "English", "fi-gb"),
|
||||
("tr", "Türkçe", "fi-tr"),
|
||||
};
|
||||
|
||||
/// <summary>Currently active language code (best effort, set after a switch).</summary>
|
||||
public string? CurrentLanguage { get; private set; }
|
||||
|
||||
/// <summary>Fires whenever the language changes and strings are reloaded.</summary>
|
||||
public event Action? Changed;
|
||||
|
||||
/// <summary>
|
||||
/// Get a localized string by key. Returns the key itself if not found
|
||||
/// (compatible with the legacy <c>_localizer["..."].Value</c> behavior).
|
||||
/// </summary>
|
||||
public string this[string key]
|
||||
{
|
||||
get => _strings.TryGetValue(key, out var v) ? v : key;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Format a localized template with positional arguments (e.g. "{0}", "{1}").
|
||||
/// </summary>
|
||||
public string Format(string key, params object?[] args)
|
||||
{
|
||||
var template = this[key];
|
||||
try
|
||||
{
|
||||
return string.Format(template, args);
|
||||
}
|
||||
catch (FormatException)
|
||||
{
|
||||
return template;
|
||||
}
|
||||
}
|
||||
|
||||
public IReadOnlyDictionary<string, string> All => _strings;
|
||||
|
||||
/// <summary>
|
||||
/// Loads localization strings from the API. Safe to call multiple times;
|
||||
/// concurrent callers share the same in-flight request.
|
||||
/// </summary>
|
||||
public Task EnsureLoadedAsync(CancellationToken ct = default)
|
||||
{
|
||||
lock (_gate)
|
||||
{
|
||||
return _loadTask ??= LoadCoreAsync(ct);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>Forces a reload (e.g. after a language change).</summary>
|
||||
public Task ReloadAsync(CancellationToken ct = default)
|
||||
{
|
||||
lock (_gate)
|
||||
{
|
||||
_loadTask = LoadCoreAsync(ct);
|
||||
return _loadTask;
|
||||
}
|
||||
}
|
||||
|
||||
private async Task LoadCoreAsync(CancellationToken ct)
|
||||
{
|
||||
// Refresh the current language alongside the strings so the
|
||||
// footer dropdown reflects the cookie value picked up by the API.
|
||||
var langTask = _api.GetLanguageAsync(ct);
|
||||
var dict = await _api.GetLocalizationStringsAsync(ct);
|
||||
if (dict is not null)
|
||||
_strings = new Dictionary<string, string>(dict, StringComparer.OrdinalIgnoreCase);
|
||||
else
|
||||
_logger.LogWarning("Localization strings could not be loaded; falling back to keys.");
|
||||
CurrentLanguage = await langTask ?? CurrentLanguage;
|
||||
Changed?.Invoke();
|
||||
}
|
||||
|
||||
public async Task ChangeLanguageAsync(string language, CancellationToken ct = default)
|
||||
{
|
||||
await _api.SetLanguageAsync(language, ct);
|
||||
CurrentLanguage = language;
|
||||
await ReloadAsync(ct);
|
||||
}
|
||||
}
|
||||
@@ -1,34 +0,0 @@
|
||||
using EnvelopeGenerator.ReceiverUI.Web.Client.Api.Models;
|
||||
|
||||
namespace EnvelopeGenerator.ReceiverUI.Web.Client.Services;
|
||||
|
||||
/// <summary>
|
||||
/// Holds the current receiver authentication context for the active envelope.
|
||||
/// Scoped per circuit (Interactive Server) or per browser tab (Interactive WASM).
|
||||
///
|
||||
/// Pages observe <see cref="Changed"/> to re-render when the underlying
|
||||
/// auth response transitions (e.g. requires_access_code -> requires_tfa
|
||||
/// -> show_document).
|
||||
/// </summary>
|
||||
public class ReceiverAuthState
|
||||
{
|
||||
private ReceiverAuthResponse? _current;
|
||||
public string? EnvelopeKey { get; private set; }
|
||||
public ReceiverAuthResponse? Current => _current;
|
||||
|
||||
public event Action? Changed;
|
||||
|
||||
public void Set(string envelopeKey, ReceiverAuthResponse? response)
|
||||
{
|
||||
EnvelopeKey = envelopeKey;
|
||||
_current = response;
|
||||
Changed?.Invoke();
|
||||
}
|
||||
|
||||
public void Clear()
|
||||
{
|
||||
EnvelopeKey = null;
|
||||
_current = null;
|
||||
Changed?.Invoke();
|
||||
}
|
||||
}
|
||||
@@ -1,25 +0,0 @@
|
||||
using Azure;
|
||||
using Azure.AI.OpenAI;
|
||||
using Microsoft.Extensions.AI;
|
||||
|
||||
namespace EnvelopeGenerator.ReceiverUI.Web.Client.Utils
|
||||
{
|
||||
public static class ServiceExtensions
|
||||
{
|
||||
// Demo AI services are rate limited and intended for demonstration purposes only.
|
||||
// DevExpress does not offer a REST API and does not ship any built-in LLMs/SLMs.
|
||||
// Use of demo credentials in production is strictly prohibited.
|
||||
// Specify the Azure OpenAI endpoint, key, and deployment name in the 'appsettings.json' file.
|
||||
public static void AddChatClient(this IServiceCollection services, string aiEndpoint, string aiKey, string deployment)
|
||||
{
|
||||
services.AddDevExpressAI();
|
||||
services.AddScoped<IChatClient>(_ =>
|
||||
{
|
||||
var azureClient = new AzureOpenAIClient(new Uri(aiEndpoint), new AzureKeyCredential(aiKey));
|
||||
return azureClient
|
||||
.GetChatClient(deployment)
|
||||
.AsIChatClient();
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,77 +0,0 @@
|
||||
using Microsoft.AspNetCore.Components;
|
||||
|
||||
namespace EnvelopeGenerator.ReceiverUI.Web.Client.Shared
|
||||
{
|
||||
public abstract class DrawerStateComponentBase : ComponentBase
|
||||
{
|
||||
[SupplyParameterFromQuery(Name = DrawerStateUrlBuilder.DrawerStateQueryParameterName)]
|
||||
public bool ToggledDrawer { get; set; }
|
||||
|
||||
[Inject] NavigationManager NavigationManager { get; set; } = null!;
|
||||
|
||||
protected string AddDrawerStateToUrl(string baseUrl)
|
||||
{
|
||||
return DrawerStateUrlBuilder.AddStateToUrl(baseUrl, ToggledDrawer, NavigationManager);
|
||||
}
|
||||
|
||||
protected string AddDrawerStateToUrlToggled(string baseUrl)
|
||||
{
|
||||
return DrawerStateUrlBuilder.AddStateToUrl(baseUrl, !ToggledDrawer, NavigationManager);
|
||||
}
|
||||
|
||||
protected string RemoveDrawerStateFromUrl(string baseUrl)
|
||||
{
|
||||
return DrawerStateUrlBuilder.RemoveStateFromUrl(baseUrl, NavigationManager);
|
||||
}
|
||||
}
|
||||
|
||||
public abstract class DrawerStateLayoutComponentBase : LayoutComponentBase
|
||||
{
|
||||
[SupplyParameterFromQuery(Name = DrawerStateUrlBuilder.DrawerStateQueryParameterName)]
|
||||
public bool ToggledDrawer { get; set; }
|
||||
|
||||
[Inject] NavigationManager NavigationManager { get; set; } = null!;
|
||||
|
||||
protected string AddDrawerStateToUrl(string baseUrl)
|
||||
{
|
||||
return DrawerStateUrlBuilder.AddStateToUrl(baseUrl, ToggledDrawer, NavigationManager);
|
||||
}
|
||||
|
||||
protected string AddDrawerStateToUrlToggled(string baseUrl)
|
||||
{
|
||||
return DrawerStateUrlBuilder.AddStateToUrl(baseUrl, !ToggledDrawer, NavigationManager);
|
||||
}
|
||||
|
||||
protected string RemoveDrawerStateFromUrl(string baseUrl)
|
||||
{
|
||||
return DrawerStateUrlBuilder.RemoveStateFromUrl(baseUrl, NavigationManager);
|
||||
}
|
||||
}
|
||||
|
||||
internal static class DrawerStateUrlBuilder
|
||||
{
|
||||
public const string DrawerStateQueryParameterName = "toggledSidebar";
|
||||
|
||||
public static string AddStateToUrl(string baseUrl, bool toggledDrawer, NavigationManager navigationManager)
|
||||
{
|
||||
return navigationManager.GetUriWithQueryParameters(
|
||||
baseUrl,
|
||||
new Dictionary<string, object?>
|
||||
{
|
||||
[DrawerStateQueryParameterName] = toggledDrawer ? true : null
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
public static string RemoveStateFromUrl(string baseUrl, NavigationManager navigationManager)
|
||||
{
|
||||
return navigationManager.GetUriWithQueryParameters(
|
||||
baseUrl,
|
||||
new Dictionary<string, object?>
|
||||
{
|
||||
[DrawerStateQueryParameterName] = null
|
||||
}
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,19 +0,0 @@
|
||||
@using System.Net.Http
|
||||
@using System.Net.Http.Json
|
||||
@using Microsoft.AspNetCore.Components.Forms
|
||||
@using Microsoft.AspNetCore.Components.Routing
|
||||
@using Microsoft.AspNetCore.Components.Web
|
||||
@using Microsoft.AspNetCore.Components.Web.Virtualization
|
||||
@using Microsoft.JSInterop
|
||||
@using static Microsoft.AspNetCore.Components.Web.RenderMode
|
||||
@using EnvelopeGenerator.ReceiverUI.Web.Client
|
||||
@using EnvelopeGenerator.ReceiverUI.Web.Client.Shared
|
||||
@using EnvelopeGenerator.ReceiverUI.Web.Client.Api
|
||||
@using EnvelopeGenerator.ReceiverUI.Web.Client.Api.Models
|
||||
@using EnvelopeGenerator.ReceiverUI.Web.Client.Services
|
||||
|
||||
@using DevExpress.Blazor
|
||||
@using DevExpress.Blazor.PdfViewer
|
||||
@using DevExpress.Blazor.Reporting.Models
|
||||
@using DevExpress.AIIntegration.Blazor.Chat
|
||||
@using DevExpress.AIIntegration.Blazor.HtmlEditor
|
||||
@@ -1,8 +0,0 @@
|
||||
{
|
||||
"Logging": {
|
||||
"LogLevel": {
|
||||
"Default": "Information",
|
||||
"Microsoft.AspNetCore": "Warning"
|
||||
}
|
||||
}
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user