Compare commits
11 Commits
b536b31a4b
...
01cd8b2607
| Author | SHA1 | Date | |
|---|---|---|---|
| 01cd8b2607 | |||
| 310d2efc6e | |||
| e5402bafc9 | |||
| f517ab4cfb | |||
| 6d63f9db2b | |||
| 46c7c31e03 | |||
| 8f6db435bb | |||
| 9bf7676464 | |||
| d9556c18ca | |||
| 5b7157f422 | |||
| 119ee3ec16 |
@@ -28,7 +28,9 @@ public class MappingProfile : Profile
|
||||
CreateMap<DocReceiverElement, DocReceiverElementDto>();
|
||||
CreateMap<DocumentStatus, DocumentStatusDto>();
|
||||
CreateMap<EmailTemplate, EmailTemplateDto>();
|
||||
CreateMap<Envelope, EnvelopeDto>();
|
||||
CreateMap<Envelope, EnvelopeDto>()
|
||||
.ForMember(dest => dest.EnvelopeTypeTitle,
|
||||
opt => opt.MapFrom(src => src.Type != null ? src.Type.Title : null));
|
||||
CreateMap<Document, DocumentDto>();
|
||||
CreateMap<History, HistoryDto>().ForMember(dest => dest.ActionDate, opt => opt.MapFrom(src => src.ChangedWhen));
|
||||
CreateMap<History, HistoryCreateDto>().ForMember(dest => dest.ActionDate, opt => opt.MapFrom(src => src.ChangedWhen));
|
||||
|
||||
@@ -0,0 +1,23 @@
|
||||
using System.Net.Http.Json;
|
||||
using System.Text.Json;
|
||||
|
||||
namespace EnvelopeGenerator.Server.Client.Services;
|
||||
|
||||
/// <summary>
|
||||
/// Client-side DTO mirroring CustomImageEntry on the server.
|
||||
/// Height is a CSS value string (e.g. "28px") set in appsettings.json.
|
||||
/// </summary>
|
||||
public sealed record CustomImageDto(string Src, string Height, Dictionary<string, string> Classes)
|
||||
{
|
||||
public CustomImageDto() : this(string.Empty, "28px", new()) { }
|
||||
|
||||
/// <summary>Returns the CSS class for the given context key, or empty string if absent.</summary>
|
||||
public string GetClass(string key) =>
|
||||
Classes.TryGetValue(key, out var cls) && cls is not null ? cls : string.Empty;
|
||||
}
|
||||
|
||||
/// <summary>Client-side DTO for the full CustomImages config (App + Company logos).</summary>
|
||||
public sealed record CustomImagesDto(CustomImageDto App, CustomImageDto Company)
|
||||
{
|
||||
public CustomImagesDto() : this(new(), new()) { }
|
||||
}
|
||||
@@ -10,6 +10,7 @@
|
||||
@inject EnvelopeGenerator.Server.Client.Services.AuthService AuthService
|
||||
@inject EnvelopeGenerator.Server.Client.Services.ThemeService ThemeService
|
||||
@inject EnvelopeGenerator.Server.Client.Services.UserPreferencesService UserPreferencesService
|
||||
@inject EnvelopeGenerator.Server.Client.Services.CustomImagesService CustomImagesService
|
||||
@inject NavigationManager Navigation
|
||||
@inject IJSRuntime JSRuntime
|
||||
@inject AppVersionService AppVersion
|
||||
@@ -22,53 +23,84 @@
|
||||
<div class="sender-action-bar__inner">
|
||||
<div class="sender-title-section">
|
||||
<div class="sender-logo">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" fill="currentColor" viewBox="0 0 16 16">
|
||||
<path d="M0 4a2 2 0 0 1 2-2h12a2 2 0 0 1 2 2v8a2 2 0 0 1-2 2H2a2 2 0 0 1-2-2V4Zm2-1a1 1 0 0 0-1 1v.217l7 4.2 7-4.2V4a1 1 0 0 0-1-1H2Zm13 2.383-4.708 2.825L15 11.105V5.383Zm-.034 6.876-5.64-3.471L8 9.583l-1.326-.795-5.64 3.47A1 1 0 0 0 2 13h12a1 1 0 0 0 .966-.741ZM1 11.105l4.708-2.897L1 5.383v5.722Z"/>
|
||||
</svg>
|
||||
@* signFLOW logo from appsettings.json — fallback to envelope SVG if not loaded *@
|
||||
@if (_appLogo is not null && !string.IsNullOrEmpty(_appLogo.Src))
|
||||
{
|
||||
<img src="@_appLogo.Src"
|
||||
style="height: @_appLogo.Height"
|
||||
class="@_appLogo.GetClass("Main")"
|
||||
alt="signFLOW" />
|
||||
}
|
||||
else
|
||||
{
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" fill="currentColor" viewBox="0 0 16 16">
|
||||
<path d="M0 4a2 2 0 0 1 2-2h12a2 2 0 0 1 2 2v8a2 2 0 0 1-2 2H2a2 2 0 0 1-2-2V4Zm2-1a1 1 0 0 0-1 1v.217l7 4.2 7-4.2V4a1 1 0 0 0-1-1H2Zm13 2.383-4.708 2.825L15 11.105V5.383Zm-.034 6.876-5.64-3.471L8 9.583l-1.326-.795-5.64 3.47A1 1 0 0 0 2 13h12a1 1 0 0 0 .966-.741ZM1 11.105l4.708-2.897L1 5.383v5.722Z" />
|
||||
</svg>
|
||||
}
|
||||
</div>
|
||||
<div class="sender-title">Umschlag-Übersicht</div>
|
||||
</div>
|
||||
|
||||
@* ── Kunden-Logo (Mitte) ───────────────────────────────────────── *@
|
||||
<div class="sender-customer-logo-area">
|
||||
@if (_companyLogo is not null && !string.IsNullOrEmpty(_companyLogo.Src))
|
||||
{
|
||||
<img src="@_companyLogo.Src"
|
||||
style="height: @_companyLogo.Height"
|
||||
class="@_companyLogo.GetClass("Show")"
|
||||
alt="Kunden-Logo" />
|
||||
}
|
||||
else
|
||||
{
|
||||
<div class="sf-customer-logo-placeholder">Kunden-Logo</div>
|
||||
}
|
||||
</div>
|
||||
|
||||
@* ── Toolbar (rechts) ─────────────────────────────────────────── *@
|
||||
<div class="sender-toolbar">
|
||||
<button class="sender-btn sender-btn--primary" @onclick="CreateEnvelope" title="Neuen Umschlag erstellen">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" viewBox="0 0 16 16">
|
||||
<path d="M8 4a.5.5 0 0 1 .5.5v3h3a.5.5 0 0 1 0 1h-3v3a.5.5 0 0 1-1 0v-3h-3a.5.5 0 0 1 0-1h3v-3A.5.5 0 0 1 8 4z"/>
|
||||
<path d="M8 4a.5.5 0 0 1 .5.5v3h3a.5.5 0 0 1 0 1h-3v3a.5.5 0 0 1-1 0v-3h-3a.5.5 0 0 1 0-1h3v-3A.5.5 0 0 1 8 4z" />
|
||||
</svg>
|
||||
Neuer Umschlag
|
||||
</button>
|
||||
|
||||
<button class="sender-btn" @onclick="EditEnvelope" disabled="@(_selectedEnvelope == null || IsEnvelopeSent(_selectedEnvelope))" title="Ausgewählten Umschlag bearbeiten">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" viewBox="0 0 16 16">
|
||||
<path d="M12.146.146a.5.5 0 0 1 .708 0l3 3a.5.5 0 0 1 0 .708l-10 10a.5.5 0 0 1-.168.11l-5 2a.5.5 0 0 1-.65-.65l2-5a.5.5 0 0 1 .11-.168l10-10zM11.207 2.5 13.5 4.793 14.793 3.5 12.5 1.207 11.207 2.5zm1.586 3L10.5 3.207 4 9.707V10h.5a.5.5 0 0 1 .5.5v.5h.5a.5.5 0 0 1 .5.5v.5h.293l6.5-6.5zm-9.761 5.175-.106.106-1.528 3.821 3.821-1.528.106-.106A.5.5 0 0 1 5 12.5V12h-.5a.5.5 0 0 1-.5-.5V11h-.5a.5.5 0 0 1-.468-.325z"/>
|
||||
<path d="M12.146.146a.5.5 0 0 1 .708 0l3 3a.5.5 0 0 1 0 .708l-10 10a.5.5 0 0 1-.168.11l-5 2a.5.5 0 0 1-.65-.65l2-5a.5.5 0 0 1 .11-.168l10-10zM11.207 2.5 13.5 4.793 14.793 3.5 12.5 1.207 11.207 2.5zm1.586 3L10.5 3.207 4 9.707V10h.5a.5.5 0 0 1 .5.5v.5h.5a.5.5 0 0 1 .5.5v.5h.293l6.5-6.5zm-9.761 5.175-.106.106-1.528 3.821 3.821-1.528.106-.106A.5.5 0 0 1 5 12.5V12h-.5a.5.5 0 0 1-.5-.5V11h-.5a.5.5 0 0 1-.468-.325z" />
|
||||
</svg>
|
||||
Bearbeiten
|
||||
</button>
|
||||
|
||||
<button class="sender-btn sender-btn--danger" @onclick="DeleteEnvelope" disabled="@(_selectedEnvelope == null)" title="Ausgewählten Umschlag löschen">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" viewBox="0 0 16 16">
|
||||
<path d="M5.5 5.5A.5.5 0 0 1 6 6v6a.5.5 0 0 1-1 0V6a.5.5 0 0 1 .5-.5zm2.5 0a.5.5 0 0 1 .5.5v6a.5.5 0 0 1-1 0V6a.5.5 0 0 1 .5-.5zm3 .5a.5.5 0 0 0-1 0v6a.5.5 0 0 0 1 0V6z"/>
|
||||
<path fill-rule="evenodd" d="M14.5 3a1 1 0 0 1-1 1H13v9a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2V4h-.5a1 1 0 0 1-1-1V2a1 1 0 0 1 1-1H6a1 1 0 0 1 1-1h2a1 1 0 0 1 1 1h3.5a1 1 0 0 1 1 1v1zM4.118 4 4 4.059V13a1 1 0 0 0 1 1h6a1 1 0 0 0 1-1V4.059L11.882 4H4.118zM2.5 3V2h11v1h-11z"/>
|
||||
<path d="M5.5 5.5A.5.5 0 0 1 6 6v6a.5.5 0 0 1-1 0V6a.5.5 0 0 1 .5-.5zm2.5 0a.5.5 0 0 1 .5.5v6a.5.5 0 0 1-1 0V6a.5.5 0 0 1 .5-.5zm3 .5a.5.5 0 0 0-1 0v6a.5.5 0 0 0 1 0V6z" />
|
||||
<path fill-rule="evenodd" d="M14.5 3a1 1 0 0 1-1 1H13v9a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2V4h-.5a1 1 0 0 1-1-1V2a1 1 0 0 1 1-1H6a1 1 0 0 1 1-1h2a1 1 0 0 1 1 1h3.5a1 1 0 0 1 1 1v1zM4.118 4 4 4.059V13a1 1 0 0 0 1 1h6a1 1 0 0 0 1-1V4.059L11.882 4H4.118zM2.5 3V2h11v1h-11z" />
|
||||
</svg>
|
||||
Löschen
|
||||
</button>
|
||||
|
||||
<button class="sender-btn" @onclick="RefreshEnvelopes" disabled="@_isLoading" title="Aktualisieren">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" viewBox="0 0 16 16">
|
||||
<path fill-rule="evenodd" d="M8 3a5 5 0 1 0 4.546 2.914.5.5 0 0 1 .908-.417A6 6 0 1 1 8 2v1z"/>
|
||||
<path d="M8 4.466V.534a.25.25 0 0 1 .41-.192l2.36 1.966c.12.1.12.284 0 .384L8.41 4.658A.25.25 0 0 1 8 4.466z"/>
|
||||
<path fill-rule="evenodd" d="M8 3a5 5 0 1 0 4.546 2.914.5.5 0 0 1 .908-.417A6 6 0 1 1 8 2v1z" />
|
||||
<path d="M8 4.466V.534a.25.25 0 0 1 .41-.192l2.36 1.966c.12.1.12.284 0 .384L8.41 4.658A.25.25 0 0 1 8 4.466z" />
|
||||
</svg>
|
||||
@if (_isLoading) {
|
||||
@if (_isLoading)
|
||||
{
|
||||
<span class="spinner-border spinner-border-sm" style="width: 14px; height: 14px;"></span>
|
||||
}
|
||||
</button>
|
||||
|
||||
<button class="sender-btn sender-btn--logout" @onclick="LogoutAsync" disabled="@_isLoggingOut" title="Abmelden">
|
||||
@if (_isLoggingOut) {
|
||||
@if (_isLoggingOut)
|
||||
{
|
||||
<span class="spinner-border spinner-border-sm" style="width: 14px; height: 14px;"></span>
|
||||
} else {
|
||||
}
|
||||
else
|
||||
{
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" viewBox="0 0 16 16">
|
||||
<path fill-rule="evenodd" d="M10 12.5a.5.5 0 0 1-.5.5h-8a.5.5 0 0 1-.5-.5v-9a.5.5 0 0 1 .5-.5h8a.5.5 0 0 1 .5.5v2a.5.5 0 0 0 1 0v-2A1.5 1.5 0 0 0 9.5 2h-8A1.5 1.5 0 0 0 0 3.5v9A1.5 1.5 0 0 0 1.5 14h8a1.5 1.5 0 0 0 1.5-1.5v-2a.5.5 0 0 0-1 0v2z"/>
|
||||
<path fill-rule="evenodd" d="M15.854 8.354a.5.5 0 0 0 0-.708l-3-3a.5.5 0 0 0-.708.708L14.293 7.5H5.5a.5.5 0 0 0 0 1h8.793l-2.147 2.146a.5.5 0 0 0 .708.708l3-3z"/>
|
||||
<path fill-rule="evenodd" d="M10 12.5a.5.5 0 0 1-.5.5h-8a.5.5 0 0 1-.5-.5v-9a.5.5 0 0 1 .5-.5h8a.5.5 0 0 1 .5.5v2a.5.5 0 0 0 1 0v-2A1.5 1.5 0 0 0 9.5 2h-8A1.5 1.5 0 0 0 0 3.5v9A1.5 1.5 0 0 0 1.5 14h8a1.5 1.5 0 0 0 1.5-1.5v-2a.5.5 0 0 0-1 0v2z" />
|
||||
<path fill-rule="evenodd" d="M15.854 8.354a.5.5 0 0 0 0-.708l-3-3a.5.5 0 0 0-.708.708L14.293 7.5H5.5a.5.5 0 0 0 0 1h8.793l-2.147 2.146a.5.5 0 0 0 .708.708l3-3z" />
|
||||
</svg>
|
||||
}
|
||||
</button>
|
||||
@@ -77,7 +109,8 @@
|
||||
</div>
|
||||
|
||||
<div class="sender-content">
|
||||
@if (_isLoading && _allEnvelopes == null) {
|
||||
@if (_isLoading && _allEnvelopes == null)
|
||||
{
|
||||
<div class="d-flex justify-content-center align-items-center h-100">
|
||||
<div class="text-center">
|
||||
<div class="spinner-border text-white mb-3" style="width: 3.5rem; height: 3.5rem;" role="status">
|
||||
@@ -86,13 +119,15 @@
|
||||
<p class="text-white fw-semibold">Umschläge werden geladen...</p>
|
||||
</div>
|
||||
</div>
|
||||
} else if (_errorMessage != null) {
|
||||
}
|
||||
else if (_errorMessage != null)
|
||||
{
|
||||
<div class="error-container">
|
||||
<div class="alert alert-danger shadow-lg">
|
||||
<div class="d-flex align-items-start">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="28" height="28" fill="currentColor" class="me-3 flex-shrink-0" viewBox="0 0 16 16">
|
||||
<path d="M8 15A7 7 0 1 1 8 1a7 7 0 0 1 0 14zm0 1A8 8 0 1 0 8 0a8 8 0 0 0 0 16z"/>
|
||||
<path d="M7.002 11a1 1 0 1 1 2 0 1 1 0 0 1-2 0zM7.1 4.995a.905.905 0 1 1 1.8 0l-.35 3.507a.552.552 0 0 1-1.1 0L7.1 4.995z"/>
|
||||
<path d="M8 15A7 7 0 1 1 8 1a7 7 0 0 1 0 14zm0 1A8 8 0 1 0 8 0a8 8 0 0 0 0 16z" />
|
||||
<path d="M7.002 11a1 1 0 1 1 2 0 1 1 0 0 1-2 0zM7.1 4.995a.905.905 0 1 1 1.8 0l-.35 3.507a.552.552 0 0 1-1.1 0L7.1 4.995z" />
|
||||
</svg>
|
||||
<div>
|
||||
<h5 class="mb-2">Fehler beim Laden der Umschläge</h5>
|
||||
@@ -101,25 +136,39 @@
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
} else {
|
||||
}
|
||||
else
|
||||
{
|
||||
<div class="sender-grid-container">
|
||||
<div class="sender-tabs">
|
||||
<button class="sender-tab @(_activeTab == "active" ? "sender-tab--active" : "")" @onclick='() => _activeTab = "active"'>
|
||||
<span>Aktive Umschläge</span>
|
||||
@if (_activeEnvelopes != null) {
|
||||
@if (_activeEnvelopes != null)
|
||||
{
|
||||
<span style="opacity: 0.6; margin-left: 0.5rem;">(@_activeEnvelopes.Count())</span>
|
||||
}
|
||||
</button>
|
||||
<button class="sender-tab @(_activeTab == "completed" ? "sender-tab--active" : "")" @onclick='() => _activeTab = "completed"'>
|
||||
<span>Abgeschlossene Umschläge</span>
|
||||
@if (_completedEnvelopes != null) {
|
||||
@if (_completedEnvelopes != null)
|
||||
{
|
||||
<span style="opacity: 0.6; margin-left: 0.5rem;">(@_completedEnvelopes.Count())</span>
|
||||
}
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div class="sender-grid-wrapper">
|
||||
@if (_activeTab == "active") {
|
||||
|
||||
@* Grid persistence hint — informs users their layout is auto-saved *@
|
||||
<div class="sf-grid-persistence-hint">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="13" height="13" fill="currentColor" viewBox="0 0 16 16">
|
||||
<path d="M8 15A7 7 0 1 1 8 1a7 7 0 0 1 0 14zm0 1A8 8 0 1 0 8 0a8 8 0 0 0 0 16z" />
|
||||
<path d="m8.93 6.588-2.29.287-.082.38.45.083c.294.07.352.176.288.469l-.738 3.468c-.194.897.105 1.319.808 1.319.545 0 1.178-.252 1.465-.598l.088-.416c-.2.176-.492.246-.686.246-.275 0-.375-.193-.304-.533L8.93 6.588zM9 4.5a1 1 0 1 1-2 0 1 1 0 0 1 2 0z" />
|
||||
</svg>
|
||||
Die Größe und Position der Spalten wird automatisch für Sie gespeichert.
|
||||
</div>
|
||||
@if (_activeTab == "active")
|
||||
{
|
||||
<DxGrid Data="@_activeEnvelopes"
|
||||
@ref="_gridActive"
|
||||
ShowFilterRow="true"
|
||||
@@ -152,7 +201,8 @@
|
||||
<CellDisplayTemplate Context="cellContext">
|
||||
@{
|
||||
var envelope = cellContext.DataItem as EnvelopeDto;
|
||||
if (envelope != null) {
|
||||
if (envelope != null)
|
||||
{
|
||||
var statusInfo = GetStatusInfo(envelope.Status);
|
||||
<div class="status-badge status-badge--@statusInfo.CssClass">
|
||||
<span class="status-dot status-dot--@statusInfo.DotColor"></span>
|
||||
@@ -166,7 +216,8 @@
|
||||
<CellDisplayTemplate Context="cellContext">
|
||||
@{
|
||||
var envelope = cellContext.DataItem as EnvelopeDto;
|
||||
if (envelope != null) {
|
||||
if (envelope != null)
|
||||
{
|
||||
var receivers = envelope.EnvelopeReceivers?.ToList() ?? [];
|
||||
var signed = receivers.Count(r => r.Signed);
|
||||
var total = receivers.Count;
|
||||
@@ -174,7 +225,8 @@
|
||||
<span style="font-size: 0.875rem; color: #6b7280;">
|
||||
@signed / @total unterschrieben
|
||||
</span>
|
||||
@if (total > 0) {
|
||||
@if (total > 0)
|
||||
{
|
||||
<div style="flex: 1; min-width: 60px; max-width: 120px; height: 6px; background: #e5e7eb; border-radius: 3px; overflow: hidden;">
|
||||
<div style="height: 100%; background: linear-gradient(90deg, #81c784 0%, #66bb6a 100%); width: @((signed * 100.0 / total).ToString("F0"))%;"></div>
|
||||
</div>
|
||||
@@ -184,26 +236,37 @@
|
||||
}
|
||||
</CellDisplayTemplate>
|
||||
</DxGridDataColumn>
|
||||
<DxGridDataColumn FieldName="EnvelopeTypeTitle" Caption="Typ"
|
||||
Width="120px" AllowSort="true" />
|
||||
<DxGridDataColumn FieldName="AddedWhen" Caption="Erstellt"
|
||||
DisplayFormat="{0:dd.MM.yyyy}" Width="110px" AllowSort="true" />
|
||||
<DxGridDataColumn FieldName="ChangedWhen" Caption="Geändert am"
|
||||
DisplayFormat="{0:dd.MM.yyyy}" Width="110px" AllowSort="true" />
|
||||
</Columns>
|
||||
<DetailRowTemplate Context="detailContext">
|
||||
<div style="padding: 1rem; background: #f9fafb;">
|
||||
<h6 style="font-weight: 600; color: #374151; margin-bottom: 0.75rem;">Empfänger</h6>
|
||||
@{
|
||||
var envelope = detailContext.DataItem as EnvelopeDto;
|
||||
if (envelope?.EnvelopeReceivers?.Any() == true) {
|
||||
if (envelope?.EnvelopeReceivers?.Any() == true)
|
||||
{
|
||||
<div style="display: flex; flex-direction: column; gap: 0.5rem;">
|
||||
@foreach (var receiver in envelope.EnvelopeReceivers) {
|
||||
@foreach (var receiver in envelope.EnvelopeReceivers)
|
||||
{
|
||||
<div style="display: flex; align-items: center; gap: 1rem; padding: 0.5rem; background: white; border-radius: 6px; border: 1px solid #e5e7eb;">
|
||||
<span class="receiver-badge receiver-badge--@(receiver.Signed ? "signed" : "unsigned")" style="min-width: 100px;">
|
||||
@if (receiver.Signed) {
|
||||
@if (receiver.Signed)
|
||||
{
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="12" height="12" fill="currentColor" viewBox="0 0 16 16">
|
||||
<path d="M13.854 3.646a.5.5 0 0 1 0 .708l-7 7a.5.5 0 0 1-.708 0l-3.5-3.5a.5.5 0 1 1 .708-.708L6.5 10.293l6.646-6.647a.5.5 0 0 1 .708 0z"/>
|
||||
<path d="M13.854 3.646a.5.5 0 0 1 0 .708l-7 7a.5.5 0 0 1-.708 0l-3.5-3.5a.5.5 0 1 1 .708-.708L6.5 10.293l6.646-6.647a.5.5 0 0 1 .708 0z" />
|
||||
</svg>
|
||||
<span>Unterschrieben</span>
|
||||
} else {
|
||||
}
|
||||
else
|
||||
{
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="12" height="12" fill="currentColor" viewBox="0 0 16 16">
|
||||
<path d="M8 15A7 7 0 1 1 8 1a7 7 0 0 1 0 14zm0 1A8 8 0 1 0 8 0a8 8 0 0 0 0 16z"/>
|
||||
<path d="M4.646 4.646a.5.5 0 0 1 .708 0L8 7.293l2.646-2.647a.5.5 0 0 1 .708.708L8.707 8l2.647 2.646a.5.5 0 0 1-.708.708L8 8.707l-2.646 2.647a.5.5 0 0 1-.708-.708L7.293 8 4.646 5.354a.5.5 0 0 1 0-.708z"/>
|
||||
<path d="M8 15A7 7 0 1 1 8 1a7 7 0 0 1 0 14zm0 1A8 8 0 1 0 8 0a8 8 0 0 0 0 16z" />
|
||||
<path d="M4.646 4.646a.5.5 0 0 1 .708 0L8 7.293l2.646-2.647a.5.5 0 0 1 .708.708L8.707 8l2.647 2.646a.5.5 0 0 1-.708.708L8 8.707l-2.646 2.647a.5.5 0 0 1-.708-.708L7.293 8 4.646 5.354a.5.5 0 0 1 0-.708z" />
|
||||
</svg>
|
||||
<span>Ausstehend</span>
|
||||
}
|
||||
@@ -215,14 +278,18 @@
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
} else {
|
||||
}
|
||||
else
|
||||
{
|
||||
<p style="color: #9ca3af; font-size: 0.875rem; margin: 0;">Keine Empfänger</p>
|
||||
}
|
||||
}
|
||||
</div>
|
||||
</DetailRowTemplate>
|
||||
</DxGrid>
|
||||
} else {
|
||||
}
|
||||
else
|
||||
{
|
||||
<DxGrid Data="@_completedEnvelopes"
|
||||
@ref="_gridCompleted"
|
||||
ShowFilterRow="true"
|
||||
@@ -252,7 +319,8 @@
|
||||
<CellDisplayTemplate Context="cellContext">
|
||||
@{
|
||||
var envelope = cellContext.DataItem as EnvelopeDto;
|
||||
if (envelope != null) {
|
||||
if (envelope != null)
|
||||
{
|
||||
var statusInfo = GetStatusInfo(envelope.Status);
|
||||
<div class="status-badge status-badge--@statusInfo.CssClass">
|
||||
<span class="status-dot status-dot--@statusInfo.DotColor"></span>
|
||||
@@ -266,7 +334,8 @@
|
||||
<CellDisplayTemplate Context="cellContext">
|
||||
@{
|
||||
var envelope = cellContext.DataItem as EnvelopeDto;
|
||||
if (envelope != null) {
|
||||
if (envelope != null)
|
||||
{
|
||||
var receivers = envelope.EnvelopeReceivers?.ToList() ?? [];
|
||||
var signed = receivers.Count(r => r.Signed);
|
||||
var total = receivers.Count;
|
||||
@@ -274,7 +343,8 @@
|
||||
<span style="font-size: 0.875rem; color: #6b7280;">
|
||||
@signed / @total unterschrieben
|
||||
</span>
|
||||
@if (total > 0) {
|
||||
@if (total > 0)
|
||||
{
|
||||
<div style="flex: 1; min-width: 60px; max-width: 120px; height: 6px; background: #e5e7eb; border-radius: 3px; overflow: hidden;">
|
||||
<div style="height: 100%; background: linear-gradient(90deg, #81c784 0%, #66bb6a 100%); width: @((signed * 100.0 / total).ToString("F0"))%;"></div>
|
||||
</div>
|
||||
@@ -284,26 +354,37 @@
|
||||
}
|
||||
</CellDisplayTemplate>
|
||||
</DxGridDataColumn>
|
||||
<DxGridDataColumn FieldName="EnvelopeTypeTitle" Caption="Typ"
|
||||
Width="120px" AllowSort="true" />
|
||||
<DxGridDataColumn FieldName="AddedWhen" Caption="Erstellt"
|
||||
DisplayFormat="{0:dd.MM.yyyy}" Width="110px" AllowSort="true" />
|
||||
<DxGridDataColumn FieldName="ChangedWhen" Caption="Geändert am"
|
||||
DisplayFormat="{0:dd.MM.yyyy}" Width="110px" AllowSort="true" />
|
||||
</Columns>
|
||||
<DetailRowTemplate Context="detailContext">
|
||||
<div style="padding: 1rem; background: #f9fafb;">
|
||||
<h6 style="font-weight: 600; color: #374151; margin-bottom: 0.75rem;">Empfänger</h6>
|
||||
@{
|
||||
var envelope = detailContext.DataItem as EnvelopeDto;
|
||||
if (envelope?.EnvelopeReceivers?.Any() == true) {
|
||||
if (envelope?.EnvelopeReceivers?.Any() == true)
|
||||
{
|
||||
<div style="display: flex; flex-direction: column; gap: 0.5rem;">
|
||||
@foreach (var receiver in envelope.EnvelopeReceivers) {
|
||||
@foreach (var receiver in envelope.EnvelopeReceivers)
|
||||
{
|
||||
<div style="display: flex; align-items: center; gap: 1rem; padding: 0.5rem; background: white; border-radius: 6px; border: 1px solid #e5e7eb;">
|
||||
<span class="receiver-badge receiver-badge--@(receiver.Signed ? "signed" : "unsigned")" style="min-width: 100px;">
|
||||
@if (receiver.Signed) {
|
||||
@if (receiver.Signed)
|
||||
{
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="12" height="12" fill="currentColor" viewBox="0 0 16 16">
|
||||
<path d="M13.854 3.646a.5.5 0 0 1 0 .708l-7 7a.5.5 0 0 1-.708 0l-3.5-3.5a.5.5 0 1 1 .708-.708L6.5 10.293l6.646-6.647a.5.5 0 0 1 .708 0z"/>
|
||||
<path d="M13.854 3.646a.5.5 0 0 1 0 .708l-7 7a.5.5 0 0 1-.708 0l-3.5-3.5a.5.5 0 1 1 .708-.708L6.5 10.293l6.646-6.647a.5.5 0 0 1 .708 0z" />
|
||||
</svg>
|
||||
<span>Unterschrieben</span>
|
||||
} else {
|
||||
}
|
||||
else
|
||||
{
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="12" height="12" fill="currentColor" viewBox="0 0 16 16">
|
||||
<path d="M8 15A7 7 0 1 1 8 1a7 7 0 0 1 0 14zm0 1A8 8 0 1 0 8 0a8 8 0 0 0 0 16z"/>
|
||||
<path d="M4.646 4.646a.5.5 0 0 1 .708 0L8 7.293l2.646-2.647a.5.5 0 0 1 .708.708L8.707 8l2.647 2.646a.5.5 0 0 1-.708.708L8 8.707l-2.646 2.647a.5.5 0 0 1-.708-.708L7.293 8 4.646 5.354a.5.5 0 0 1 0-.708z"/>
|
||||
<path d="M8 15A7 7 0 1 1 8 1a7 7 0 0 1 0 14zm0 1A8 8 0 1 0 8 0a8 8 0 0 0 0 16z" />
|
||||
<path d="M4.646 4.646a.5.5 0 0 1 .708 0L8 7.293l2.646-2.647a.5.5 0 0 1 .708.708L8.707 8l2.647 2.646a.5.5 0 0 1-.708.708L8 8.707l-2.646 2.647a.5.5 0 0 1-.708-.708L7.293 8 4.646 5.354a.5.5 0 0 1 0-.708z" />
|
||||
</svg>
|
||||
<span>Ausstehend</span>
|
||||
}
|
||||
@@ -315,7 +396,9 @@
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
} else {
|
||||
}
|
||||
else
|
||||
{
|
||||
<p style="color: #9ca3af; font-size: 0.875rem; margin: 0;">Keine Empfänger</p>
|
||||
}
|
||||
}
|
||||
@@ -341,8 +424,12 @@
|
||||
private DxGrid? _gridActive;
|
||||
private DxGrid? _gridCompleted;
|
||||
|
||||
// Logo config loaded from /api/CustomImages after auth
|
||||
private EnvelopeGenerator.Server.Client.Services.CustomImageDto? _appLogo;
|
||||
private EnvelopeGenerator.Server.Client.Services.CustomImageDto? _companyLogo;
|
||||
|
||||
// Stable keys used to store/restore each grid's layout in UserPreferences.GridLayouts
|
||||
private const string GridKeyActive = "sender.active-envelopes";
|
||||
private const string GridKeyActive = "sender.active-envelopes";
|
||||
private const string GridKeyCompleted = "sender.completed-envelopes";
|
||||
|
||||
protected override async Task OnInitializedAsync()
|
||||
@@ -360,6 +447,11 @@
|
||||
try { await ThemeService.LoadFromServerAsync(); }
|
||||
catch { /* swallow — default theme remains active */ }
|
||||
|
||||
// Load logo config — authenticated, so this should succeed
|
||||
var logos = await CustomImagesService.GetAsync();
|
||||
_appLogo = logos.App;
|
||||
_companyLogo = logos.Company;
|
||||
|
||||
await LoadEnvelopesAsync();
|
||||
}
|
||||
|
||||
@@ -372,7 +464,7 @@
|
||||
try
|
||||
{
|
||||
_allEnvelopes = await EnvelopeService.GetAsync() ?? [];
|
||||
|
||||
|
||||
// Split into active and completed based on status
|
||||
var envelopes = _allEnvelopes.ToList();
|
||||
_activeEnvelopes = envelopes.Where(e => ((EnvelopeStatus)e.Status).IsActive()).ToList();
|
||||
|
||||
@@ -1,15 +1,27 @@
|
||||
@page "/"
|
||||
@inject IJSRuntime JS
|
||||
@inject NavigationManager Navigation
|
||||
@inject EnvelopeGenerator.Server.Client.Services.CustomImagesService CustomImagesService
|
||||
@rendermode InteractiveWebAssembly
|
||||
|
||||
<div class="home-page-wrapper">
|
||||
|
||||
<div class="home-hero-header">
|
||||
<div class="home-hero-header__inner">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="48" height="48" fill="currentColor" class="home-hero-header__icon" viewBox="0 0 16 16">
|
||||
<path d="M0 4a2 2 0 0 1 2-2h12a2 2 0 0 1 2 2v8a2 2 0 0 1-2 2H2a2 2 0 0 1-2-2V4Zm2-1a1 1 0 0 0-1 1v.217l7 4.2 7-4.2V4a1 1 0 0 0-1-1H2Zm13 2.383-4.708 2.825L15 11.105V5.383Zm-.034 6.876-5.64-3.471L8 9.583l-1.326-.795-5.64 3.47A1 1 0 0 0 2 13h12a1 1 0 0 0 .966-.741ZM1 11.105l4.708-2.897L1 5.383v5.722Z"/>
|
||||
</svg>
|
||||
@* signFLOW logo from appsettings.json CustomImages.App — fallback to SVG if not loaded *@
|
||||
@if (_appLogo is not null && !string.IsNullOrEmpty(_appLogo.Src))
|
||||
{
|
||||
<img src="@_appLogo.Src"
|
||||
style="height: @_appLogo.Height"
|
||||
class="home-hero-header__icon @_appLogo.GetClass("Main")"
|
||||
alt="signFLOW" />
|
||||
}
|
||||
else
|
||||
{
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="48" height="48" fill="currentColor" class="home-hero-header__icon" viewBox="0 0 16 16">
|
||||
<path d="M0 4a2 2 0 0 1 2-2h12a2 2 0 0 1 2 2v8a2 2 0 0 1-2 2H2a2 2 0 0 1-2-2V4Zm2-1a1 1 0 0 0-1 1v.217l7 4.2 7-4.2V4a1 1 0 0 0-1-1H2Zm13 2.383-4.708 2.825L15 11.105V5.383Zm-.034 6.876-5.64-3.471L8 9.583l-1.326-.795-5.64 3.47A1 1 0 0 0 2 13h12a1 1 0 0 0 .966-.741ZM1 11.105l4.708-2.897L1 5.383v5.722Z"/>
|
||||
</svg>
|
||||
}
|
||||
<div>
|
||||
<h1 class="home-hero-header__title">SignFlow</h1>
|
||||
<p class="home-hero-header__subtitle">Willkommen im eSign-Portal</p>
|
||||
@@ -73,11 +85,18 @@
|
||||
"Mit seiner benutzerfreundlichen Oberfläche können Sie Ihre Dokumente schnell hochladen, die Unterschriftsprozesse verfolgen und Ihre digitalen Unterschriftenanwendungen einfach durchführen. " +
|
||||
"Dieses Portal beschleunigt Ihren Arbeitsablauf mit rechtlich gültigen Unterschriften und erhöht gleichzeitig die Sicherheit Ihrer Dokumente.";
|
||||
|
||||
private EnvelopeGenerator.Server.Client.Services.CustomImageDto? _appLogo;
|
||||
|
||||
protected override async Task OnInitializedAsync()
|
||||
{
|
||||
// Load logo config early — 401 here is normal (not yet logged in), service returns defaults silently
|
||||
var logos = await CustomImagesService.GetAsync();
|
||||
_appLogo = logos.App;
|
||||
}
|
||||
|
||||
protected override async Task OnAfterRenderAsync(bool firstRender)
|
||||
{
|
||||
if (firstRender)
|
||||
{
|
||||
await JS.InvokeVoidAsync("receiverSignature.startTyped", "home-description", HomePageDescription, 15);
|
||||
}
|
||||
if (!firstRender) return;
|
||||
await JS.InvokeVoidAsync("receiverSignature.startTyped", "home-description", HomePageDescription, 15);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -38,6 +38,9 @@ builder.Services.AddScoped<EnvelopeService>();
|
||||
builder.Services.AddScoped<UserPreferencesService>();
|
||||
builder.Services.AddScoped<ThemeService>();
|
||||
|
||||
// Logo configuration (loaded once after login, in-memory cached)
|
||||
builder.Services.AddScoped<CustomImagesService>();
|
||||
|
||||
// DevExpress WASM
|
||||
builder.Services.AddDevExpressWebAssemblyBlazorPdfViewer();
|
||||
builder.Services.AddDevExpressWebAssemblyBlazorReportViewer();
|
||||
|
||||
@@ -0,0 +1,55 @@
|
||||
using System.Net.Http.Json;
|
||||
using System.Text.Json;
|
||||
|
||||
namespace EnvelopeGenerator.Server.Client.Services;
|
||||
|
||||
/// <summary>
|
||||
/// Fetches and caches the CustomImages configuration (signFLOW + company logos)
|
||||
/// from GET /api/CustomImages.
|
||||
///
|
||||
/// Config changes rarely (deployment-time only), so results are cached in-memory
|
||||
/// for the lifetime of the service (WASM app session). No TTL needed.
|
||||
/// </summary>
|
||||
public class CustomImagesService(IHttpClientFactory clientFactory)
|
||||
{
|
||||
private static readonly JsonSerializerOptions _json = new(JsonSerializerDefaults.Web);
|
||||
private const string Endpoint = "/api/CustomImages";
|
||||
|
||||
private CustomImagesDto? _cached;
|
||||
|
||||
/// <summary>
|
||||
/// Returns logo configuration. Fetches once from server; all subsequent calls
|
||||
/// are served from memory with zero network overhead.
|
||||
/// Returns defaults (empty Src) on auth failure (401) — components must handle gracefully.
|
||||
/// </summary>
|
||||
public async Task<CustomImagesDto> GetAsync(CancellationToken ct = default)
|
||||
{
|
||||
if (_cached is not null)
|
||||
return _cached;
|
||||
|
||||
try
|
||||
{
|
||||
var client = clientFactory.CreateClient("EnvelopeGenerator.Server");
|
||||
var response = await client.GetAsync(Endpoint, ct);
|
||||
|
||||
if (!response.IsSuccessStatusCode)
|
||||
{
|
||||
// Not authenticated yet (login page) — return defaults silently.
|
||||
_cached = new CustomImagesDto();
|
||||
return _cached;
|
||||
}
|
||||
|
||||
_cached = await response.Content.ReadFromJsonAsync<CustomImagesDto>(_json, ct)
|
||||
?? new CustomImagesDto();
|
||||
return _cached;
|
||||
}
|
||||
catch (Exception)
|
||||
{
|
||||
// Network error or server unavailable — return defaults so UI still renders.
|
||||
return new CustomImagesDto();
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>Call after logout to force re-fetch on next login.</summary>
|
||||
public void Invalidate() => _cached = null;
|
||||
}
|
||||
@@ -10,6 +10,23 @@
|
||||
<link rel="stylesheet" href="css/envelope-viewer.css" />
|
||||
<link rel="stylesheet" href="EnvelopeGenerator.Server.styles.css" />
|
||||
<HeadOutlet />
|
||||
<!-- Apply saved theme immediately on page load — before WASM boots.
|
||||
Reads localStorage["signflow.theme"] to avoid flash of default theme. -->
|
||||
<script>
|
||||
(function () {
|
||||
var allowed = { 'blazing-dark': true, 'blazing-berry': true, 'purple': true };
|
||||
var theme = localStorage.getItem('signflow.theme');
|
||||
if (!theme || !allowed[theme]) theme = 'blazing-berry';
|
||||
var bsTheme = theme === 'blazing-dark' ? 'dark' : 'light';
|
||||
document.documentElement.setAttribute('data-sf-theme', theme);
|
||||
document.documentElement.setAttribute('data-bs-theme', bsTheme);
|
||||
var link = document.createElement('link');
|
||||
link.id = 'dx-theme-link';
|
||||
link.rel = 'stylesheet';
|
||||
link.href = '_content/DevExpress.Blazor.Themes/' + theme + '.bs5.min.css';
|
||||
document.head.appendChild(link);
|
||||
})();
|
||||
</script>
|
||||
</head>
|
||||
|
||||
<body>
|
||||
@@ -18,6 +35,7 @@
|
||||
<script src="js/typed.umd.js"></script>
|
||||
<script src="js/receiver-signature.js?v=9"></script>
|
||||
<script src="js/signflow-theme.js"></script>
|
||||
<script src="js/envelope-editor.js"></script>
|
||||
<script src="_framework/blazor.web.js"></script>
|
||||
</body>
|
||||
|
||||
|
||||
@@ -81,8 +81,8 @@
|
||||
@if (_signatureFields.Count > 0)
|
||||
{
|
||||
<span style="display: inline-flex; align-items: center; padding: 0.125rem 0.4rem;
|
||||
background: #ede9fe; border-radius: 0.25rem; color: #6d28d9;
|
||||
font-weight: 500; font-size: 0.7rem; white-space: nowrap;">
|
||||
background: #ede9fe; border-radius: 0.25rem; color: #6d28d9;
|
||||
font-weight: 500; font-size: 0.7rem; white-space: nowrap;">
|
||||
@_signatureFields.Count Signaturfeld@(_signatureFields.Count != 1 ? "er" : "")
|
||||
</span>
|
||||
}
|
||||
@@ -118,8 +118,8 @@
|
||||
<div class="sender-receivers-list">
|
||||
@foreach (var receiver in _receivers)
|
||||
{
|
||||
<div class="sender-receiver-chip"
|
||||
style="border-left: 3px solid @receiver.Color;">
|
||||
<div class="sender-receiver-chip"
|
||||
style="border-left: 3px solid @receiver.Color;">
|
||||
<div class="sender-receiver-chip__body">
|
||||
<div class="sender-receiver-chip__name">@receiver.FullName</div>
|
||||
<div class="sender-receiver-chip__email">@receiver.Email</div>
|
||||
@@ -130,7 +130,7 @@
|
||||
</div>
|
||||
|
||||
<button class="pdf-toolbar__btn pdf-toolbar__btn--signature-change sender-toolbar-action-btn sender-toolbar-action-btn--compact
|
||||
@(_pendingReceiverForPlacement?.Id == receiver.Id ? "pdf-toolbar__btn--signature-change-active" : "")"
|
||||
@(_pendingReceiverForPlacement?.Id == receiver.Id ? "pdf-toolbar__btn--signature-change-active" : "")"
|
||||
@onclick="() => ActivatePlacementForReceiver(receiver)"
|
||||
style="@(_pendingReceiverForPlacement?.Id == receiver.Id ? $"background:{receiver.Color};color:#fff;border-color:{receiver.Color};" : $"color:{receiver.Color};border-color:{receiver.Color};")"
|
||||
title="Signaturfeld platzieren">
|
||||
@@ -211,7 +211,7 @@
|
||||
@if (_pendingReceiverForPlacement is not null)
|
||||
{
|
||||
<div style="background: @_pendingReceiverForPlacement.Color; color: white; font-size: 0.75rem; font-weight: 500;
|
||||
padding: 0.3rem 1.5rem; text-align: center; letter-spacing: 0.01em;">
|
||||
padding: 0.3rem 1.5rem; text-align: center; letter-spacing: 0.01em;">
|
||||
📌 Klicken Sie auf die Stelle im Dokument für <strong>@_pendingReceiverForPlacement.FullName</strong>.
|
||||
<button @onclick="CancelPlacement"
|
||||
style="background:none;border:none;color:white;cursor:pointer;font-size:0.75rem;text-decoration:underline;padding:0;">
|
||||
@@ -229,8 +229,8 @@
|
||||
<div class="d-flex justify-content-center align-items-center h-100">
|
||||
<div class="text-center" style="max-width: 420px;">
|
||||
<div style="width: 72px; height: 72px; background: rgba(255,255,255,0.15);
|
||||
border-radius: 50%; display: flex; align-items: center;
|
||||
justify-content: center; margin: 0 auto 1.25rem;">
|
||||
border-radius: 50%; display: flex; align-items: center;
|
||||
justify-content: center; margin: 0 auto 1.25rem;">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="36" height="36" fill="white" viewBox="0 0 16 16">
|
||||
<path d="M14 4.5V14a2 2 0 0 1-2 2H4a2 2 0 0 1-2-2V2a2 2 0 0 1 2-2h5.5L14 4.5zm-3 0A1.5 1.5 0 0 1 9.5 3V1H4a1 1 0 0 0-1 1v12a1 1 0 0 0 1 1h8a1 1 0 0 0 1-1V4.5h-2z" />
|
||||
</svg>
|
||||
@@ -316,13 +316,13 @@
|
||||
@if (_receiverEmailSuggestions.Count > 0)
|
||||
{
|
||||
<div class="sender-receiver-popup__suggestions">
|
||||
<DxListBox TData="string"
|
||||
TValue="string"
|
||||
Data="@_receiverEmailSuggestions"
|
||||
Value="@_selectedReceiverEmailSuggestion"
|
||||
ValueChanged="OnReceiverEmailSuggestionCommittedAsync"
|
||||
SelectionMode="ListBoxSelectionMode.Single"
|
||||
CssClass="w-100" />
|
||||
<DxListBox TData="string"
|
||||
TValue="string"
|
||||
Data="@_receiverEmailSuggestions"
|
||||
Value="@_selectedReceiverEmailSuggestion"
|
||||
ValueChanged="OnReceiverEmailSuggestionCommittedAsync"
|
||||
SelectionMode="ListBoxSelectionMode.Single"
|
||||
CssClass="w-100" />
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
@@ -370,9 +370,9 @@
|
||||
{
|
||||
<div style="display:flex;align-items:flex-start;gap:1rem;padding:0.5rem 0;">
|
||||
<div style="flex-shrink:0;width:40px;height:40px;background:#d1fae5;border-radius:50%;
|
||||
display:flex;align-items:center;justify-content:center;">
|
||||
display:flex;align-items:center;justify-content:center;">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" fill="#065f46" viewBox="0 0 16 16">
|
||||
<path d="M13.854 3.646a.5.5 0 0 1 0 .708l-7 7a.5.5 0 0 1-.708 0l-3.5-3.5a.5.5 0 1 1 .708-.708L6.5 10.293l6.646-6.647a.5.5 0 0 1 .708 0z"/>
|
||||
<path d="M13.854 3.646a.5.5 0 0 1 0 .708l-7 7a.5.5 0 0 1-.708 0l-3.5-3.5a.5.5 0 1 1 .708-.708L6.5 10.293l6.646-6.647a.5.5 0 0 1 .708 0z" />
|
||||
</svg>
|
||||
</div>
|
||||
<div>
|
||||
@@ -389,10 +389,10 @@
|
||||
{
|
||||
<div style="display:flex;align-items:flex-start;gap:1rem;padding:0.5rem 0;">
|
||||
<div style="flex-shrink:0;width:40px;height:40px;background:#fee2e2;border-radius:50%;
|
||||
display:flex;align-items:center;justify-content:center;">
|
||||
display:flex;align-items:center;justify-content:center;">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" fill="#991b1b" viewBox="0 0 16 16">
|
||||
<path d="M8 15A7 7 0 1 1 8 1a7 7 0 0 1 0 14zm0 1A8 8 0 1 0 8 0a8 8 0 0 0 0 16z"/>
|
||||
<path d="M7.002 11a1 1 0 1 1 2 0 1 1 0 0 1-2 0zM7.1 4.995a.905.905 0 1 1 1.8 0l-.35 3.507a.552.552 0 0 1-1.1 0L7.1 4.995z"/>
|
||||
<path d="M8 15A7 7 0 1 1 8 1a7 7 0 0 1 0 14zm0 1A8 8 0 1 0 8 0a8 8 0 0 0 0 16z" />
|
||||
<path d="M7.002 11a1 1 0 1 1 2 0 1 1 0 0 1-2 0zM7.1 4.995a.905.905 0 1 1 1.8 0l-.35 3.507a.552.552 0 0 1-1.1 0L7.1 4.995z" />
|
||||
</svg>
|
||||
</div>
|
||||
<div>
|
||||
@@ -413,7 +413,7 @@
|
||||
<button class="btn btn-primary"
|
||||
@onclick="GoToDashboard"
|
||||
style="background:linear-gradient(135deg,#059669,#047857);border:none;border-radius:6px;
|
||||
padding:0.5rem 1.5rem;font-weight:600;color:#fff;">
|
||||
padding:0.5rem 1.5rem;font-weight:600;color:#fff;">
|
||||
Zur Übersicht
|
||||
</button>
|
||||
}
|
||||
@@ -436,7 +436,7 @@
|
||||
|
||||
// ── Constants ──
|
||||
// Signature field size in PDF points (fixed): 1.77" × 1.96"
|
||||
const double SigWidthPt = 1.77 * 72; // 127.44 pt
|
||||
const double SigWidthPt = 1.77 * 72; // 127.44 pt
|
||||
const double SigHeightPt = 1.96 * 72; // 141.12 pt
|
||||
|
||||
// CssClass for DxPdfViewer — used by JS to locate page elements
|
||||
@@ -447,8 +447,8 @@
|
||||
|
||||
// ── State ──
|
||||
DxPdfViewer? _pdfViewer;
|
||||
bool _pdfLoaded = false;
|
||||
string _fileName = string.Empty;
|
||||
bool _pdfLoaded = false;
|
||||
string _fileName = string.Empty;
|
||||
string? _errorMessage;
|
||||
byte[]? _pdfBytes; // Current rendered PDF (original + placeholders burned in)
|
||||
byte[]? _originalPdfBytes; // Pristine upload — never modified, used as base for redraw
|
||||
@@ -457,25 +457,25 @@
|
||||
ReceiverDraft? _pendingReceiverForPlacement; // Set when user clicks "Signatur hinzufügen"
|
||||
|
||||
// ── Save state ──
|
||||
bool _isSaving = false;
|
||||
bool _savePopupVisible = false;
|
||||
string? _saveErrorMessage = null;
|
||||
bool _isSaving = false;
|
||||
bool _savePopupVisible = false;
|
||||
string? _saveErrorMessage = null;
|
||||
|
||||
// ── Envelope metadata ──
|
||||
string _envelopeTitle = string.Empty;
|
||||
string _envelopeTitle = string.Empty;
|
||||
string _envelopeMessage = string.Empty;
|
||||
bool _titleTouched = false;
|
||||
bool _titleTouched = false;
|
||||
|
||||
List<ReceiverDraft> _receivers = [];
|
||||
bool _receiverPopupVisible;
|
||||
string _receiverDraftName = string.Empty;
|
||||
string _receiverDraftEmail = string.Empty;
|
||||
bool _receiverPopupVisible;
|
||||
string _receiverDraftName = string.Empty;
|
||||
string _receiverDraftEmail = string.Empty;
|
||||
string _receiverDraftPhoneNumber = string.Empty;
|
||||
string? _selectedReceiverEmailSuggestion;
|
||||
string? _receiverPopupValidationMessage;
|
||||
bool _isReceiverEmailSearchRunning;
|
||||
bool _isReceiverEmailSearchRunning;
|
||||
List<string> _receiverEmailSuggestions = [];
|
||||
int _receiverEmailSearchVersion;
|
||||
int _receiverEmailSearchVersion;
|
||||
|
||||
static readonly System.ComponentModel.DataAnnotations.EmailAddressAttribute ReceiverEmailValidator = new();
|
||||
|
||||
@@ -512,12 +512,12 @@
|
||||
&& cached is not null)
|
||||
{
|
||||
_originalPdfBytes = cached.OriginalPdfBytes;
|
||||
_signatureFields = cached.Fields;
|
||||
_fileName = cached.FileName;
|
||||
_pdfLoaded = _originalPdfBytes is { Length: > 0 };
|
||||
_receivers = cached.Receivers;
|
||||
_envelopeTitle = cached.Title;
|
||||
_envelopeMessage = cached.Message;
|
||||
_signatureFields = cached.Fields;
|
||||
_fileName = cached.FileName;
|
||||
_pdfLoaded = _originalPdfBytes is { Length: > 0 };
|
||||
_receivers = cached.Receivers;
|
||||
_envelopeTitle = cached.Title;
|
||||
_envelopeMessage = cached.Message;
|
||||
|
||||
// Redraw placeholders onto the original PDF
|
||||
if (_pdfLoaded)
|
||||
@@ -545,8 +545,8 @@
|
||||
await file.OpenReadStream(maxBytes).CopyToAsync(ms);
|
||||
|
||||
_originalPdfBytes = ms.ToArray();
|
||||
_fileName = file.Name;
|
||||
_pdfLoaded = true;
|
||||
_fileName = file.Name;
|
||||
_pdfLoaded = true;
|
||||
_signatureFields.Clear();
|
||||
_pendingReceiverForPlacement = null;
|
||||
|
||||
@@ -597,12 +597,12 @@
|
||||
double pageHeightPt;
|
||||
try
|
||||
{
|
||||
using var ms = new System.IO.MemoryStream(_originalPdfBytes);
|
||||
var doc = PdfSharp.Pdf.IO.PdfReader.Open(ms, PdfSharp.Pdf.IO.PdfDocumentOpenMode.Import);
|
||||
using var ms = new System.IO.MemoryStream(_originalPdfBytes);
|
||||
var doc = PdfSharp.Pdf.IO.PdfReader.Open(ms, PdfSharp.Pdf.IO.PdfDocumentOpenMode.Import);
|
||||
int pageIndex = Math.Max(0, Math.Min(coords.PageIndex, doc.PageCount - 1));
|
||||
var page = doc.Pages[pageIndex];
|
||||
pageWidthPt = page.Width.Point;
|
||||
pageHeightPt = page.Height.Point;
|
||||
var page = doc.Pages[pageIndex];
|
||||
pageWidthPt = page.Width.Point;
|
||||
pageHeightPt = page.Height.Point;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
@@ -614,17 +614,17 @@
|
||||
double xPt = coords.NormX * pageWidthPt;
|
||||
double yPt = coords.NormY * pageHeightPt;
|
||||
|
||||
xPt = Math.Max(0, Math.Min(xPt, pageWidthPt - SigWidthPt));
|
||||
xPt = Math.Max(0, Math.Min(xPt, pageWidthPt - SigWidthPt));
|
||||
yPt = Math.Max(0, Math.Min(yPt, pageHeightPt - SigHeightPt));
|
||||
|
||||
int page1Based = coords.PageIndex + 1;
|
||||
|
||||
var field = new SignatureFieldDraft(
|
||||
XPt: xPt,
|
||||
YPt: yPt,
|
||||
Page: page1Based,
|
||||
XPt: xPt,
|
||||
YPt: yPt,
|
||||
Page: page1Based,
|
||||
ReceiverName: _pendingReceiverForPlacement.FullName,
|
||||
Color: _pendingReceiverForPlacement.Color);
|
||||
Color: _pendingReceiverForPlacement.Color);
|
||||
|
||||
_signatureFields.Add(field);
|
||||
_pendingReceiverForPlacement = null;
|
||||
@@ -711,7 +711,7 @@
|
||||
string.Join(", ", receiversWithoutField.Select(r => r.FullName)));
|
||||
}
|
||||
|
||||
_isSaving = true;
|
||||
_isSaving = true;
|
||||
_saveErrorMessage = null;
|
||||
await InvokeAsync(StateHasChanged);
|
||||
|
||||
@@ -728,16 +728,16 @@
|
||||
var fields = _signatureFields
|
||||
.Where(f => f.ReceiverName == receiver.FullName)
|
||||
.Select(f => new DocReceiverElementCreateDto(
|
||||
X: f.XPt / 72.0,
|
||||
Y: f.YPt / 72.0,
|
||||
X: f.XPt / 72.0,
|
||||
Y: f.YPt / 72.0,
|
||||
Page: f.Page))
|
||||
.ToList();
|
||||
|
||||
return new ReceiverGetOrCreateCommand
|
||||
{
|
||||
EmailAddress = receiver.Email,
|
||||
Salution = receiver.FullName,
|
||||
PhoneNumber = string.IsNullOrWhiteSpace(receiver.PhoneNumber)
|
||||
EmailAddress = receiver.Email,
|
||||
Salution = receiver.FullName,
|
||||
PhoneNumber = string.IsNullOrWhiteSpace(receiver.PhoneNumber)
|
||||
? null
|
||||
: receiver.PhoneNumber,
|
||||
DocReceiverElements = fields,
|
||||
@@ -746,13 +746,13 @@
|
||||
|
||||
var command = new CreateEnvelopeReceiverCommand
|
||||
{
|
||||
Title = _envelopeTitle.Trim(),
|
||||
Message = string.IsNullOrWhiteSpace(_envelopeMessage)
|
||||
Title = _envelopeTitle.Trim(),
|
||||
Message = string.IsNullOrWhiteSpace(_envelopeMessage)
|
||||
? "Bitte unterzeichnen Sie das beigefügte Dokument."
|
||||
: _envelopeMessage.Trim(),
|
||||
TFAEnabled = false,
|
||||
Document = new DocumentCreateCommand { DataAsBase64 = docBase64 },
|
||||
Receivers = receiversCmd,
|
||||
Document = new DocumentCreateCommand { DataAsBase64 = docBase64 },
|
||||
Receivers = receiversCmd,
|
||||
};
|
||||
|
||||
var result = await EnvelopeReceiverService.CreateAsync(command);
|
||||
@@ -784,11 +784,11 @@
|
||||
|
||||
var data = new EditorSessionData(
|
||||
OriginalPdfBytes: _originalPdfBytes ?? [],
|
||||
Fields: [.. _signatureFields],
|
||||
FileName: _fileName,
|
||||
Receivers: [.. _receivers],
|
||||
Title: _envelopeTitle,
|
||||
Message: _envelopeMessage);
|
||||
Fields: [.. _signatureFields],
|
||||
FileName: _fileName,
|
||||
Receivers: [.. _receivers],
|
||||
Title: _envelopeTitle,
|
||||
Message: _envelopeMessage);
|
||||
|
||||
MemoryCache.Set(SessionKey, data, SessionTtl);
|
||||
}
|
||||
@@ -798,7 +798,7 @@
|
||||
{
|
||||
if (fields.Count == 0) return originalPdf;
|
||||
|
||||
using var inputMs = new System.IO.MemoryStream(originalPdf);
|
||||
using var inputMs = new System.IO.MemoryStream(originalPdf);
|
||||
using var outputMs = new System.IO.MemoryStream();
|
||||
|
||||
var document = PdfSharp.Pdf.IO.PdfReader.Open(
|
||||
@@ -819,11 +819,11 @@
|
||||
var nameBrush = new PdfSharp.Drawing.XSolidBrush(HexToXColor(field.Color, alpha: 230));
|
||||
|
||||
var fontLabel = new PdfSharp.Drawing.XFont("Arial", 9, PdfSharp.Drawing.XFontStyleEx.Bold);
|
||||
var fontName = new PdfSharp.Drawing.XFont("Arial", 7, PdfSharp.Drawing.XFontStyleEx.Regular);
|
||||
var fontName = new PdfSharp.Drawing.XFont("Arial", 7, PdfSharp.Drawing.XFontStyleEx.Regular);
|
||||
|
||||
var fmtCenter = new PdfSharp.Drawing.XStringFormat
|
||||
{
|
||||
Alignment = PdfSharp.Drawing.XStringAlignment.Center,
|
||||
Alignment = PdfSharp.Drawing.XStringAlignment.Center,
|
||||
LineAlignment = PdfSharp.Drawing.XLineAlignment.Center,
|
||||
};
|
||||
|
||||
@@ -861,11 +861,11 @@
|
||||
// ── Receiver popup ──
|
||||
void OpenAddReceiverPopup()
|
||||
{
|
||||
_receiverDraftName = string.Empty;
|
||||
_receiverDraftEmail = string.Empty;
|
||||
_receiverDraftName = string.Empty;
|
||||
_receiverDraftEmail = string.Empty;
|
||||
_receiverDraftPhoneNumber = string.Empty;
|
||||
_selectedReceiverEmailSuggestion = null;
|
||||
_receiverPopupValidationMessage = null;
|
||||
_receiverPopupValidationMessage = null;
|
||||
_receiverEmailSuggestions.Clear();
|
||||
_receiverPopupVisible = true;
|
||||
}
|
||||
@@ -873,9 +873,9 @@
|
||||
void CloseAddReceiverPopup()
|
||||
{
|
||||
_receiverPopupVisible = false;
|
||||
_receiverPopupValidationMessage = null;
|
||||
_receiverPopupValidationMessage = null;
|
||||
_selectedReceiverEmailSuggestion = null;
|
||||
_isReceiverEmailSearchRunning = false;
|
||||
_isReceiverEmailSearchRunning = false;
|
||||
}
|
||||
|
||||
void OnReceiverNameChanged(string? value)
|
||||
@@ -947,7 +947,7 @@
|
||||
{
|
||||
_receiverDraftEmail = value?.Trim() ?? string.Empty;
|
||||
_selectedReceiverEmailSuggestion = _receiverDraftEmail;
|
||||
_receiverPopupValidationMessage = null;
|
||||
_receiverPopupValidationMessage = null;
|
||||
|
||||
var searchVersion = ++_receiverEmailSearchVersion;
|
||||
|
||||
@@ -955,7 +955,7 @@
|
||||
{
|
||||
_receiverEmailSuggestions.Clear();
|
||||
_selectedReceiverEmailSuggestion = null;
|
||||
_isReceiverEmailSearchRunning = false;
|
||||
_isReceiverEmailSearchRunning = false;
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -994,8 +994,8 @@
|
||||
|
||||
Task SaveReceiverAsync()
|
||||
{
|
||||
var fullName = _receiverDraftName.Trim();
|
||||
var email = _receiverDraftEmail.Trim();
|
||||
var fullName = _receiverDraftName.Trim();
|
||||
var email = _receiverDraftEmail.Trim();
|
||||
var phoneNumber = _receiverDraftPhoneNumber.Trim();
|
||||
|
||||
if (string.IsNullOrWhiteSpace(fullName))
|
||||
@@ -1034,12 +1034,12 @@
|
||||
record ReceiverDraft(Guid Id, string FullName, string Email, string PhoneNumber, string Color);
|
||||
|
||||
record EditorSessionData(
|
||||
byte[] OriginalPdfBytes,
|
||||
byte[] OriginalPdfBytes,
|
||||
List<SignatureFieldDraft> Fields,
|
||||
string FileName,
|
||||
List<ReceiverDraft> Receivers,
|
||||
string Title,
|
||||
string Message);
|
||||
string FileName,
|
||||
List<ReceiverDraft> Receivers,
|
||||
string Title,
|
||||
string Message);
|
||||
|
||||
// ── Receiver colour palette (cycles when > 8 receivers) ──
|
||||
static readonly string[] ReceiverPalette =
|
||||
|
||||
@@ -0,0 +1,26 @@
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Microsoft.Extensions.Options;
|
||||
using EnvelopeGenerator.Domain.Constants;
|
||||
using EnvelopeGenerator.Server.Models;
|
||||
|
||||
namespace EnvelopeGenerator.Server.Controllers;
|
||||
|
||||
/// <summary>
|
||||
/// Exposes the CustomImages configuration (signFLOW logo + company logo) to WASM clients.
|
||||
/// Values are sourced from appsettings.json "CustomImages" section via Options Pattern.
|
||||
///
|
||||
/// To change logos per deployment: update "CustomImages.App.Src", "App.Height",
|
||||
/// "Company.Src", "Company.Height" in appsettings.json — no code change required.
|
||||
/// </summary>
|
||||
[ApiController]
|
||||
[Route("api/[controller]")]
|
||||
[Authorize(Policy = AuthPolicy.SenderOrReceiver)]
|
||||
public class CustomImagesController(IOptions<CustomImagesOptions> options) : ControllerBase
|
||||
{
|
||||
/// <summary>
|
||||
/// Returns the signFLOW app logo and company logo configuration.
|
||||
/// </summary>
|
||||
[HttpGet]
|
||||
public ActionResult<CustomImagesOptions> Get() => Ok(options.Value);
|
||||
}
|
||||
@@ -10,9 +10,9 @@
|
||||
<Authors>Digital Data GmbH</Authors>
|
||||
<Company>Digital Data GmbH</Company>
|
||||
<Product>EnvelopeGenerator.Server</Product>
|
||||
<Version>1.0.1-beta</Version>
|
||||
<FileVersion>1.0.1.0</FileVersion>
|
||||
<AssemblyVersion>1.0.1.0</AssemblyVersion>
|
||||
<Version>1.1.0-beta</Version>
|
||||
<FileVersion>1.1.0.0</FileVersion>
|
||||
<AssemblyVersion>1.1.0.0</AssemblyVersion>
|
||||
<Copyright>Copyright © 2026 Digital Data GmbH. All rights reserved.</Copyright>
|
||||
<DocumentationFile>bin\$(Configuration)\$(TargetFramework)\$(AssemblyName).xml</DocumentationFile>
|
||||
</PropertyGroup>
|
||||
|
||||
@@ -1,6 +1,36 @@
|
||||
namespace EnvelopeGenerator.Server.Models;
|
||||
|
||||
public class CustomImages : Dictionary<string, Image>
|
||||
/// <summary>
|
||||
/// A single image asset entry — source path and optional per-context CSS classes.
|
||||
/// Height controls the rendered size in CSS units (e.g. "28px", "2rem").
|
||||
/// Change Src and Height in appsettings.json per deployment; no code change required.
|
||||
/// </summary>
|
||||
public sealed class CustomImageEntry
|
||||
{
|
||||
public new Image this[string key] => TryGetValue(key, out var img) && img is not null ? img : new();
|
||||
}
|
||||
public string Src { get; set; } = string.Empty;
|
||||
|
||||
/// <summary>
|
||||
/// CSS height value, e.g. "28px" or "2rem".
|
||||
/// Allows different-sized logos across deployments without CSS changes.
|
||||
/// </summary>
|
||||
public string Height { get; set; } = "28px";
|
||||
|
||||
public Dictionary<string, string> Classes { get; set; } = new();
|
||||
|
||||
/// <summary>Returns the CSS class for the given context key, or empty string if absent.</summary>
|
||||
public string GetClass(string key) =>
|
||||
Classes.TryGetValue(key, out var cls) && cls is not null ? cls : string.Empty;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Bound from appsettings.json "CustomImages" section via Options Pattern.
|
||||
/// App = signFLOW application logo.
|
||||
/// Company = customer/company logo (Digital Data GmbH by default).
|
||||
/// </summary>
|
||||
public sealed class CustomImagesOptions
|
||||
{
|
||||
public const string SectionName = "CustomImages";
|
||||
|
||||
public CustomImageEntry App { get; set; } = new();
|
||||
public CustomImageEntry Company { get; set; } = new();
|
||||
}
|
||||
|
||||
@@ -326,12 +326,17 @@ try
|
||||
// User Preferences (theme, dark mode, grid layouts — stored in TBDD_CACHE)
|
||||
// Registration handled by Infrastructure DependencyInjection (AddEnvelopeGeneratorInfrastructureServices)
|
||||
|
||||
// CustomImages — logo sources and heights, configurable per deployment via appsettings.json
|
||||
builder.Services.Configure<EnvelopeGenerator.Server.Models.CustomImagesOptions>(
|
||||
config.GetSection(EnvelopeGenerator.Server.Models.CustomImagesOptions.SectionName));
|
||||
|
||||
// Client-side services that MainLayout depends on must also be registered here
|
||||
// so that Blazor Auto prerender (SSR pass) can resolve them on the server.
|
||||
// ThemeService.InitializeAsync() uses IJSRuntime but is only called in OnAfterRenderAsync
|
||||
// (never during prerender), so registering it server-side is safe.
|
||||
builder.Services.AddScoped<EnvelopeGenerator.Server.Client.Services.UserPreferencesService>();
|
||||
builder.Services.AddScoped<EnvelopeGenerator.Server.Client.Services.ThemeService>();
|
||||
builder.Services.AddScoped<EnvelopeGenerator.Server.Client.Services.CustomImagesService>();
|
||||
|
||||
// Business Services (Server specific)
|
||||
builder.Services.AddScoped<DocumentService>();
|
||||
|
||||
@@ -17,7 +17,7 @@
|
||||
"http://localhost:7192"
|
||||
],
|
||||
"ConnectionStrings": {
|
||||
"Default": "Server=SDD-VMP04-SQL17\\DD_DEVELOP01;Database=DD_ECM;User Id=sa;Password=dd;Encrypt=false;TrustServerCertificate=True;",
|
||||
"Default": "Server=SDD-VHP04-SQL19\\DD_TESTING01;Database=DD_ECM;User Id=sa;Password=123456789dD!;Encrypt=false;TrustServerCertificate=True;",
|
||||
"DbMigrationTest": "Server=SDD-VMP04-SQL17\\DD_DEVELOP01;Database=DD_ECM_DATA_MIGR_TEST;User Id=sa;Password=dd;Encrypt=false;TrustServerCertificate=True;"
|
||||
},
|
||||
"DirectorySearchOptions": {
|
||||
@@ -145,15 +145,17 @@
|
||||
"CustomImages": {
|
||||
"App": {
|
||||
"Src": "/img/DD_signFLOW_LOGO.png",
|
||||
"Height": "28px",
|
||||
"Classes": {
|
||||
"Main": "signFlow-logo"
|
||||
"Main": "signflow-app-logo"
|
||||
}
|
||||
},
|
||||
"Company": {
|
||||
"Src": "/img/digital_data.svg",
|
||||
"Height": "33px",
|
||||
"Classes": {
|
||||
"Show": "dd-show-logo",
|
||||
"Locked": "dd-locked-logo"
|
||||
"Show": "signflow-company-logo",
|
||||
"Locked": "signflow-company-logo"
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
@@ -333,6 +333,24 @@ article {
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
/* ── Logos ───────────────────────────────────────────────────────────────── */
|
||||
/* Heights are controlled by inline style="height: Xpx" from appsettings.json */
|
||||
|
||||
.signflow-app-logo {
|
||||
width: auto;
|
||||
object-fit: contain;
|
||||
display: block;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.signflow-company-logo {
|
||||
width: auto;
|
||||
object-fit: contain;
|
||||
display: block;
|
||||
opacity: 0.88;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
/* ── Footer ──────────────────────────────────────────────────────────────── */
|
||||
.receiver-footer {
|
||||
position: fixed;
|
||||
|
||||
@@ -28,6 +28,34 @@
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 1rem;
|
||||
flex-shrink: 0; /* don't shrink — logo area in the middle gets the space */
|
||||
}
|
||||
|
||||
/* ── Customer logo area (middle of action bar) ──────────────────────────── */
|
||||
.sender-customer-logo-area {
|
||||
flex: 1; /* takes up remaining space between left and right */
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
min-width: 0; /* allow flex child to shrink below content size */
|
||||
}
|
||||
|
||||
/* Placeholder shown when no customer logo is configured */
|
||||
.sf-customer-logo-placeholder {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
height: 28px;
|
||||
min-width: 90px;
|
||||
padding: 0 12px;
|
||||
border: 1px dashed rgba(126, 34, 206, 0.28);
|
||||
border-radius: 6px;
|
||||
font-size: 0.65rem;
|
||||
font-weight: 500;
|
||||
color: rgba(126, 34, 206, 0.38);
|
||||
white-space: nowrap;
|
||||
letter-spacing: 0.03em;
|
||||
text-transform: uppercase;
|
||||
user-select: none;
|
||||
}
|
||||
|
||||
.sender-logo svg {
|
||||
@@ -171,7 +199,20 @@
|
||||
}
|
||||
|
||||
.sender-grid-wrapper {
|
||||
padding: 1.5rem 2rem 2rem;
|
||||
padding: 0 2rem 2rem; /* top padding removed — hint banner provides top spacing */
|
||||
}
|
||||
|
||||
/* ── Grid persistence hint banner ─────────────────────────────────────────── */
|
||||
.sf-grid-persistence-hint {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
padding: 6px 2rem;
|
||||
font-size: 0.72rem;
|
||||
color: rgba(126, 34, 206, 0.65);
|
||||
background: rgba(126, 34, 206, 0.04);
|
||||
border-bottom: 1px solid rgba(126, 34, 206, 0.08);
|
||||
user-select: none;
|
||||
}
|
||||
|
||||
/* Hide DevExpress empty cells */
|
||||
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 7.6 KiB |
@@ -0,0 +1,12 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<svg id="Ebene_1" data-name="Ebene 1" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 850 121.32">
|
||||
<defs>
|
||||
<style>
|
||||
.cls-1 {
|
||||
fill: #a52431;
|
||||
stroke-width: 0px;
|
||||
}
|
||||
</style>
|
||||
</defs>
|
||||
<path class="cls-1" d="M83.01,60.66c0-35.12-12.77-57.47-41.51-57.47H0v114.94h39.91c30.33,0,43.1-20.75,43.1-57.47M49.49,60.66c0,19.16-1.6,28.73-15.96,28.73V31.93c14.37,0,15.96,12.77,15.96,28.73M126.11,3.19h-33.52v114.94h33.52V3.19ZM223.49,97.38v-46.29h-39.91l-1.6,23.95h12.77v9.58c-3.19,4.79-6.39,7.98-12.77,7.98-7.98,0-12.77-11.17-12.77-31.93s4.79-31.93,12.77-31.93c6.86,0,11.33,5.75,12.77,14.37l28.73-6.39C218.7,15.96,207.52,0,181.98,0c-33.52,0-46.29,28.73-46.29,60.66s12.77,60.66,46.29,60.66c17.56,0,31.93-7.98,41.51-23.94M268.18,3.19h-33.52v114.94h33.52V3.19ZM346.4,31.93l-3.19-28.74h-65.45l-3.19,28.74h19.16v86.2h33.52V31.93h19.16ZM427.81,118.13L399.07,3.19h-31.93l-28.73,114.94h35.12l1.6-14.37h15.96l1.6,14.37h35.12ZM387.58,76.62h-8.94l3.51-28.74c.32-3.19.64-6.38.8-9.58h.32c.16,3.19.48,6.38.8,9.58l3.51,28.74ZM493.26,89.4h-25.54V3.19h-33.52v114.94h55.87l3.19-28.74ZM614.58,60.66c0-35.12-12.77-57.47-41.51-57.47h-41.51v114.94h39.91c30.33,0,43.1-20.75,43.1-57.47M581.05,60.66c0,19.16-1.6,28.73-15.96,28.73V31.93c14.37,0,15.96,12.77,15.96,28.73M704.75,118.13l-28.73-114.94h-31.93l-28.74,114.94h35.12l1.6-14.37h15.96l1.6,14.37h35.12ZM664.52,76.62h-8.94l3.51-28.74c.32-3.19.64-6.38.8-9.58h.32c.16,3.19.48,6.38.8,9.58l3.51,28.74ZM768.59,31.93l-3.19-28.74h-65.45l-3.19,28.74h19.16v86.2h33.52V31.93h19.16ZM850,118.13l-28.73-114.94h-31.93l-28.73,114.94h35.12l1.6-14.37h15.96l1.6,14.37h35.12ZM809.77,76.62h-8.94l3.51-28.74c.32-3.19.64-6.38.8-9.58h.32c.16,3.19.48,6.38.8,9.58l3.51,28.74Z"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 1.7 KiB |
415
EnvelopeGenerator.Server/SF-74-PLAN.md
Normal file
415
EnvelopeGenerator.Server/SF-74-PLAN.md
Normal file
@@ -0,0 +1,415 @@
|
||||
# SF-74 — signFLOW: Theme, Dark Mode, Grid Persistence, Logo & Dashboard
|
||||
|
||||
**Ticket:** SF-74 | **Bearbeiter:** Hakan Tek | **Status:** In Bearbeitung
|
||||
**Letzte Aktualisierung:** 2026-09-03
|
||||
**Erstellt von:** Marlon Schreiber | **Verknüpft mit:** SF-71
|
||||
|
||||
---
|
||||
|
||||
## Originale Anforderungen
|
||||
|
||||
### Marlon Schreiber (Ticket-Text):
|
||||
1. **signFLOW-Logo** + **Kunden-Logo-Bereich** auf der Startseite
|
||||
2. **DevExpress Skin-Auswahl** — mindestens 3 Themes, vom User wählbar + gespeichert
|
||||
3. **Grid-Layout-Persistenz** — Spaltenbreite/Position pro User gespeichert
|
||||
|
||||
### Screenshot-Annotationen (`/sender`):
|
||||
```
|
||||
[signFLOW Logo] Umschlag-Übersicht [Kunden-Logo] [+Neuer Umschlag][Bearb.][Lösch.][↺][→|]
|
||||
signFLOW Portal-Übersicht [Zurückrufen/löschen][Dok.anzeigen][Excel]
|
||||
|
||||
[Info-Banner: "Die Größe und Position der Spalten muss verändert und für den User gespeichert..."]
|
||||
|
||||
ID ↓ Titel Status [Typ] Empfänger [Erstellt] [Geändert am]
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Projektstruktur — Clean Architecture
|
||||
|
||||
```
|
||||
EnvelopeGenerator.Domain/ → Entities, Domain-Interfaces
|
||||
EnvelopeGenerator.Application/ → Service-Interfaces, DTOs, Use-Cases
|
||||
EnvelopeGenerator.Infrastructure/ → Cache, DB, Implementierungen
|
||||
EnvelopeGenerator.Server/ → Blazor SSR Host, Controller, Middleware
|
||||
EnvelopeGenerator.Server.Client/ → Blazor WASM Client
|
||||
```
|
||||
|
||||
**Abhängigkeitsregel:** `Domain ← Application ← Infrastructure ← Server / Server.Client`
|
||||
|
||||
---
|
||||
|
||||
## ✅ Abgeschlossene Phasen (1–5)
|
||||
|
||||
| Phase | Inhalt | Status |
|
||||
|-------|--------|--------|
|
||||
| 1 | Server: UserPreferences API (TBDD_CACHE, `IUserPreferencesService`) | ✅ |
|
||||
| 2 | WASM: `ThemeService`, `UserPreferencesService` (1h In-Memory-Cache) | ✅ |
|
||||
| 3 | Footer Theme Slider (🌙 blazing-dark ── blazing-berry ── purple ☀️) | ✅ |
|
||||
| 4 | Grid Layout Persistence (`LayoutAutoSaving`/`LayoutAutoLoading`) | ✅ |
|
||||
| 5 | Theme-CSS-Overrides für alle Seiten (`[data-sf-theme]`) | ✅ |
|
||||
|
||||
**Key decisions:** TBDD_CACHE statt eigener DB-Tabelle · `SenderOrReceiver` Policy · Slider statt Toggle · `signflow.theme` in localStorage · DevExpress 25.2.3 hat keine `.dark.`-Suffix-Dateien
|
||||
|
||||
---
|
||||
|
||||
## 🔲 Phase 6a — signFLOW-Logo
|
||||
|
||||
### Ziel
|
||||
signFLOW-App-Logo im Sender Dashboard (`/sender`) Action Bar links neben dem Titel, sowie auf der Startseite (`/`) im Hero-Header.
|
||||
|
||||
### Technischer Ansatz
|
||||
|
||||
**Options Pattern** — `appsettings.json` → `CustomImagesOptions` → DI → Controller → WASM Service.
|
||||
|
||||
```
|
||||
appsettings.json
|
||||
└── CustomImagesOptions (Options Pattern, IOptions<>)
|
||||
└── GET /api/CustomImages (neuer Controller, auth: SenderOrReceiver)
|
||||
└── CustomImagesService (WASM, in-memory cache, kein TTL)
|
||||
└── EnvelopeSenderPage + IndexPage
|
||||
```
|
||||
|
||||
**Warum kein `wwwroot/appsettings.json`?**
|
||||
Statische Assets sind public (keine Auth). Logo-Pfade sind deployment-spezifisch.
|
||||
Options Pattern erlaubt Produktionswechsel ohne Code-Änderung.
|
||||
|
||||
### Neue/geänderte Dateien
|
||||
|
||||
#### `Server/Models/CustomImages.cs` — NEU
|
||||
|
||||
```csharp
|
||||
public sealed class CustomImageEntry
|
||||
{
|
||||
public string Src { get; set; } = string.Empty;
|
||||
/// <summary>CSS height, e.g. "28px" or "2rem". Change per deployment in appsettings.</summary>
|
||||
public string Height { get; set; } = "28px";
|
||||
public Dictionary<string, string> Classes { get; set; } = new();
|
||||
public string GetClass(string key) =>
|
||||
Classes.TryGetValue(key, out var cls) ? cls ?? string.Empty : string.Empty;
|
||||
}
|
||||
|
||||
public sealed class CustomImagesOptions
|
||||
{
|
||||
public const string SectionName = "CustomImages";
|
||||
public CustomImageEntry App { get; set; } = new();
|
||||
public CustomImageEntry Company { get; set; } = new();
|
||||
}
|
||||
```
|
||||
|
||||
#### `appsettings.json` — Änderung (Zeile 145)
|
||||
|
||||
```json
|
||||
"CustomImages": {
|
||||
"App": { "Src": "/img/DD_signFLOW_LOGO.png", "Height": "28px",
|
||||
"Classes": { "Main": "signflow-app-logo" } },
|
||||
"Company": { "Src": "/img/digital_data.svg", "Height": "22px",
|
||||
"Classes": { "Show": "signflow-company-logo" } }
|
||||
}
|
||||
```
|
||||
|
||||
> **Deployment:** Nur `Src` + `Height` in `appsettings.json` ändern — keine Code-Anpassung nötig.
|
||||
|
||||
#### `Server/Controllers/CustomImagesController.cs` — NEU
|
||||
|
||||
```
|
||||
GET /api/CustomImages
|
||||
Auth: [Authorize(Policy = AuthPolicy.SenderOrReceiver)]
|
||||
Returns: CustomImagesOptions
|
||||
```
|
||||
|
||||
#### `Server/Program.cs` — DI
|
||||
|
||||
```csharp
|
||||
builder.Services.Configure<CustomImagesOptions>(
|
||||
config.GetSection(CustomImagesOptions.SectionName));
|
||||
```
|
||||
|
||||
#### `Server/wwwroot/img/` — Neue Dateien
|
||||
|
||||
Kopiert aus `EnvelopeGenerator.Web/wwwroot/img/`:
|
||||
- `DD_signFLOW_LOGO.png`
|
||||
- `digital_data.svg`
|
||||
|
||||
#### `Client/Services/CustomImagesService.cs` — NEU
|
||||
|
||||
```csharp
|
||||
// GET /api/CustomImages → einmalig laden, in-memory gecacht (Config ändert sich kaum)
|
||||
public class CustomImagesService(IHttpClientFactory factory)
|
||||
{
|
||||
private CustomImagesDto? _cached;
|
||||
public async Task<CustomImagesDto> GetAsync(CancellationToken ct = default) { ... }
|
||||
}
|
||||
```
|
||||
|
||||
#### `app.css` — Neue Klassen
|
||||
|
||||
```css
|
||||
.signflow-app-logo { height: var(--sf-app-height, 28px); width: auto; object-fit: contain; }
|
||||
.signflow-company-logo { height: var(--sf-company-height, 22px); width: auto; object-fit: contain; opacity: .88; }
|
||||
```
|
||||
|
||||
#### `IndexPage.razor` — Änderung
|
||||
|
||||
```razor
|
||||
<!-- Alt: <svg class="home-hero-header__icon"> -->
|
||||
<!-- Neu: -->
|
||||
<img src="@_appLogo.Src" style="height: @_appLogo.Height"
|
||||
class="home-hero-header__icon" alt="signFLOW" />
|
||||
```
|
||||
|
||||
#### `EnvelopeSenderPage.razor` — signFLOW Logo im Action Bar
|
||||
|
||||
```razor
|
||||
<div class="sender-branding">
|
||||
<img src="@_appLogo.Src" style="height: @_appLogo.Height"
|
||||
class="@_appLogo.GetClass("Main")" alt="signFLOW" />
|
||||
<div class="sender-title-block">
|
||||
<div class="sender-title">Umschlag-Übersicht</div>
|
||||
<div class="sender-subtitle">signFLOW Portal - Übersicht</div>
|
||||
</div>
|
||||
</div>
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 🔲 Phase 6b — Kunden-Logo Placeholder
|
||||
|
||||
### Ziel
|
||||
Mittlerer Bereich im Action Bar zeigt das Kunden-Logo (aus Config) oder einen Platzhalter.
|
||||
|
||||
### Implementierung
|
||||
|
||||
**Config-Quelle:** `CustomImagesOptions.Company` (aus Phase 6a bereits geladen)
|
||||
|
||||
```razor
|
||||
<!-- Mitte des Action Bars -->
|
||||
<div class="sender-customer-logo-area">
|
||||
@if (!string.IsNullOrEmpty(_companyLogo?.Src))
|
||||
{
|
||||
<img src="@_companyLogo.Src" style="height: @_companyLogo.Height"
|
||||
class="@_companyLogo.GetClass("Show")" alt="Kunden-Logo" />
|
||||
}
|
||||
else
|
||||
{
|
||||
<div class="sf-customer-logo-placeholder">Kunden-Logo</div>
|
||||
}
|
||||
</div>
|
||||
```
|
||||
|
||||
#### `app.css` — Neue Klasse
|
||||
|
||||
```css
|
||||
.sf-customer-logo-placeholder {
|
||||
height: 24px; min-width: 90px;
|
||||
border: 1px dashed rgba(126, 34, 206, 0.3); border-radius: 4px;
|
||||
display: flex; align-items: center; padding: 0 10px;
|
||||
font-size: 0.62rem; color: rgba(126, 34, 206, 0.4); white-space: nowrap;
|
||||
}
|
||||
```
|
||||
|
||||
#### `sender-page.css` — Neues Layout (3-Bereich-Action-Bar)
|
||||
|
||||
```css
|
||||
.sender-branding { display: flex; align-items: center; gap: .75rem; flex-shrink: 0; }
|
||||
.sender-subtitle { font-size: .72rem; color: #6b7280; font-weight: 400; }
|
||||
.sender-customer-logo-area { flex: 1; display: flex; justify-content: center; }
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 🔲 Phase 6c — Grid: Fehlende Spalten + Persistence Hint
|
||||
|
||||
### Fehlende Spalten (aus Screenshot-Annotationen)
|
||||
|
||||
**Verfügbarkeit in `EnvelopeDto`:**
|
||||
- `EnvelopeTypeTitle` (string?) → `spalte für "Typ"` ✅
|
||||
- `AddedWhen` (DateTime) → `spalte für "erstellt"` ✅
|
||||
- `ChangedWhen` (DateTime?) → `Spalte "zuletzt geändert am"` ✅
|
||||
|
||||
> ⚠️ **Vor Implementierung prüfen:** `EnvelopeTypeTitle` wird aus dem Navigation Property `EnvelopeType` gemappt. Mapping-Konfiguration in `Application/...MappingProfile.cs` verifizieren.
|
||||
|
||||
#### `EnvelopeSenderPage.razor` — Neue Spalten (beide Grids)
|
||||
|
||||
```razor
|
||||
<DxGridDataColumn FieldName="EnvelopeTypeTitle" Caption="Typ"
|
||||
Width="120px" AllowSort="true" />
|
||||
<DxGridDataColumn FieldName="AddedWhen" Caption="Erstellt"
|
||||
DisplayFormat="{0:dd.MM.yyyy}" Width="110px" AllowSort="true" />
|
||||
<DxGridDataColumn FieldName="ChangedWhen" Caption="Geändert am"
|
||||
DisplayFormat="{0:dd.MM.yyyy}" Width="110px" AllowSort="true" />
|
||||
```
|
||||
|
||||
### Grid Persistence Hint
|
||||
|
||||
Info-Banner über dem Grid:
|
||||
|
||||
```razor
|
||||
<div class="sf-grid-persistence-hint">
|
||||
<svg><!-- info icon --></svg>
|
||||
Die Größe und Position der Spalten wird automatisch für Sie gespeichert.
|
||||
</div>
|
||||
```
|
||||
|
||||
```css
|
||||
.sf-grid-persistence-hint {
|
||||
display: flex; align-items: center; gap: 6px;
|
||||
padding: 5px 16px; font-size: 0.72rem;
|
||||
color: rgba(126, 34, 206, 0.7);
|
||||
background: rgba(126, 34, 206, 0.04);
|
||||
border-bottom: 1px solid rgba(126, 34, 206, 0.08);
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 🔲 Phase 6d — Dokument anzeigen (PDF Preview)
|
||||
|
||||
### Anforderung
|
||||
Ausgewähltes Envelope-Dokument im Popup anzeigen. Envelope ist nach dem Senden **read-only** (Mails wurden gesendet — nachträgliche Änderungen würden Konsistenz brechen).
|
||||
|
||||
### Technischer Ansatz
|
||||
|
||||
`DxPdfViewer` erfordert **zwingend `@rendermode InteractiveServer`**.
|
||||
`EnvelopeSenderPage` ist WASM (`@rendermode InteractiveWebAssembly`).
|
||||
|
||||
**Lösung:** `DxPopup` im WASM-Parent mit einem Server-Component darin:
|
||||
|
||||
```
|
||||
EnvelopeSenderPage.razor (@rendermode WASM)
|
||||
└── <DxPopup>
|
||||
└── <EnvelopePdfPreviewComponent @rendermode InteractiveServer>
|
||||
└── <DxPdfViewer> (SSR-only)
|
||||
```
|
||||
|
||||
#### `Server/Components/Shared/EnvelopePdfPreviewComponent.razor` — NEU
|
||||
|
||||
```razor
|
||||
@rendermode InteractiveServer
|
||||
@inject IDocumentService DocumentService <!-- oder HTTP client -->
|
||||
|
||||
<DxPdfViewer DocumentContent="@_pdfBytes" />
|
||||
|
||||
@code {
|
||||
[Parameter] public int EnvelopeId { get; set; }
|
||||
byte[]? _pdfBytes;
|
||||
|
||||
protected override async Task OnParametersSetAsync()
|
||||
{
|
||||
if (EnvelopeId > 0)
|
||||
_pdfBytes = await DocumentService.GetPdfBytesAsync(EnvelopeId);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Button: nur aktiv wenn Envelope ausgewählt UND Dokument vorhanden (`envelope.Documents?.Any() == true`).
|
||||
|
||||
---
|
||||
|
||||
## 🔲 Phase 6e — Export nach Excel
|
||||
|
||||
### Technischer Ansatz
|
||||
|
||||
`DxGrid.ExportToXlsxAsync(fileName, options)` — direkt im Razor-Code, keine Backend-Implementierung nötig.
|
||||
Download geht direkt an den Browser.
|
||||
|
||||
```csharp
|
||||
async Task ExportToExcelAsync()
|
||||
{
|
||||
var grid = _activeTab == "active" ? _gridActive : _gridCompleted;
|
||||
if (grid is null) return;
|
||||
var fileName = $"Umschlaege_{DateTime.Now:yyyy-MM-dd}.xlsx";
|
||||
await grid.ExportToXlsxAsync(fileName, new GridXlExportOptions());
|
||||
}
|
||||
```
|
||||
|
||||
> **Hinweis:** `CellDisplayTemplate`-Inhalte werden nicht exportiert (DevExpress-Limitierung).
|
||||
> Die Spalten `Status`, `EnvelopeReceivers` nutzen Templates → Wert im Export ist leer.
|
||||
> **Future Task:** Für vollständigen Export mit formatierten Zellen `CustomizeCell`-Event nutzen.
|
||||
|
||||
Button in Toolbar:
|
||||
```razor
|
||||
<button class="sender-btn" @onclick="ExportToExcelAsync"
|
||||
title="Aktuelle Ansicht als Excel exportieren">
|
||||
<!-- Excel-Icon SVG -->
|
||||
Export nach Excel
|
||||
</button>
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 🔲 Phase 6f — Action Bar Redesign
|
||||
|
||||
### Ziel-Layout (aus Screenshot)
|
||||
|
||||
```
|
||||
┌──────────────────────────────────────────────────────────────────────────────┐
|
||||
│ [Logo] Umschlag-Übersicht │ [Kunden-Logo] │ [+Neu] [Bearb.] [Lösch.] │
|
||||
│ signFLOW Portal-Übers│ │ [↺] [→|] │
|
||||
│ │ │ [Zurückrufen?] [Dok.] [XLS] │
|
||||
└──────────────────────────────────────────────────────────────────────────────┘
|
||||
```
|
||||
|
||||
### Neue CSS-Klassen in `sender-page.css`
|
||||
|
||||
```css
|
||||
.sender-toolbar-right { display: flex; flex-direction: column; gap: .35rem; align-items: flex-end; }
|
||||
.sender-toolbar-row { display: flex; align-items: center; gap: .5rem; }
|
||||
.sender-toolbar-row--primary { /* Hauptbuttons */ }
|
||||
.sender-toolbar-row--secondary { /* Dokument anzeigen, Export */ }
|
||||
```
|
||||
|
||||
### `Zurückrufen/löschen` — Offene Frage ❓
|
||||
|
||||
> **Ticket-Kommentar-Entwurf (Deutsch, für Marlon Schreiber):**
|
||||
>
|
||||
> Hallo @Marlon Schreiber,
|
||||
>
|
||||
> im Screenshot sehe ich einen Button „Zurückrufen/löschen". Könntest du den
|
||||
> Unterschied zum bereits vorhandenen „Löschen"-Button erläutern?
|
||||
>
|
||||
> Meine Vermutung: „Zurückrufen" bedeutet das Widerrufen eines **bereits gesendeten**
|
||||
> Umschlags (Status → `EnvelopeWithdrawn`), während „Löschen" einen noch nicht
|
||||
> gesendeten Umschlag entfernt. Ist das korrekt? Und falls ja:
|
||||
> - Sollen Empfänger bei einem Widerruf automatisch benachrichtigt werden?
|
||||
> - Soll der Status auf `Zurückgerufen` gesetzt werden und der Umschlag
|
||||
> weiterhin in der Liste erscheinen (aber inaktiv)?
|
||||
>
|
||||
> Bitte kurz antworten, damit ich dies korrekt implementieren kann.
|
||||
|
||||
---
|
||||
|
||||
## Implementierungsreihenfolge Phase 6
|
||||
|
||||
```
|
||||
6a (Logo) → 6b (Kunden-Logo) → 6c (Grid-Spalten) → 6d (PDF Preview) → 6e (Excel) → 6f (Action Bar)
|
||||
```
|
||||
|
||||
Jede Teilphase ist unabhängig deploybar. `6f` fasst alle Layout-Änderungen zusammen
|
||||
und sollte zuletzt implementiert werden, wenn alle Komponenten fertig sind.
|
||||
|
||||
---
|
||||
|
||||
## Bekannte Einschränkungen & Offene Punkte
|
||||
|
||||
| # | Thema | Status |
|
||||
|---|-------|--------|
|
||||
| 1 | `DxPdfViewer` → InteractiveServer zwingend | Architektonisch gelöst (6d) |
|
||||
| 2 | Excel-Export: Template-Spalten leer im Export | Akzeptiert, Future Task (CustomizeCell) |
|
||||
| 3 | `Zurückrufen/löschen` — Bedeutung unklar | Kommentar-Frage vorbereitet (6f) |
|
||||
| 4 | `EnvelopeTypeTitle` Mapping prüfen | Vor 6c verifizieren |
|
||||
| 5 | Logo-Pfade produktionsspezifisch | Via `appsettings.json` konfigurierbar |
|
||||
| 6 | Server-seitige Pages — inline Farben bleiben | Out-of-scope |
|
||||
|
||||
---
|
||||
|
||||
## Nicht in diesem Ticket
|
||||
|
||||
- `Einstellungen`-Seite (separates Ticket)
|
||||
- Mobile Responsive (separates Ticket)
|
||||
- Excel CustomizeCell vollständige Implementierung (Future Task)
|
||||
- Widerruf-Backend (wartet auf Klärung)
|
||||
|
||||
---
|
||||
|
||||
*Erstellt: 2026-09-01 | Aktualisiert: 2026-09-03 | Ticket: SF-74 | Projekt: signFLOW*
|
||||
@@ -1,5 +1,5 @@
|
||||
@echo off
|
||||
setlocal
|
||||
setlocal enabledelayedexpansion
|
||||
|
||||
echo ============================================================
|
||||
echo EnvelopeGenerator.Server - Self-Contained Publish
|
||||
@@ -10,11 +10,38 @@ echo.
|
||||
REM Must be run from the solution root directory.
|
||||
REM This file is located under: EnvelopeGenerator.Server\
|
||||
|
||||
set PROJECT=EnvelopeGenerator.Server\EnvelopeGenerator.Server.csproj
|
||||
set OUTPUT=publish-output
|
||||
set PROJECT=%~dp0EnvelopeGenerator.Server\EnvelopeGenerator.Server.csproj
|
||||
set RID=win-x64
|
||||
set FRAMEWORK=net8.0
|
||||
|
||||
REM ── Resolve project version from .csproj ──────────────────────────────────────
|
||||
REM Reads the <Version> element directly from the .csproj XML using findstr.
|
||||
REM Example: <Version>1.1.0-beta</Version> → VERSION=1.1.0-beta
|
||||
REM 'tokens=2 delims=>' splits on '>' and takes the second token (the value+tag),
|
||||
REM then 'delims=<' strips the closing '<' tag leaving the bare version string.
|
||||
for /f "tokens=2 delims=>< " %%v in ('findstr /i "<Version>" "%PROJECT%"') do (
|
||||
set VERSION=%%v
|
||||
)
|
||||
|
||||
if "%VERSION%"=="" (
|
||||
echo [ERROR] Could not read ^<Version^> from %PROJECT%. Aborting.
|
||||
pause
|
||||
exit /b 1
|
||||
)
|
||||
|
||||
REM ── Output path: network share / versioned subfolder ──────────────────────────
|
||||
REM Format: M:\App&Service\0 DD - Smart UP\signFLOW\Server\net8\{version}
|
||||
REM Note: the '&' in the path is literal — no escaping needed inside a set "..." assignment.
|
||||
set "OUTPUT=M:\App&Service\0 DD - Smart UP\signFLOW\Server\net8\%VERSION%"
|
||||
|
||||
echo [0/3] Resolved build parameters
|
||||
echo Project : %PROJECT%
|
||||
echo Version : %VERSION%
|
||||
echo Output : %OUTPUT%
|
||||
echo RID : %RID%
|
||||
echo Framework: %FRAMEWORK%
|
||||
echo.
|
||||
|
||||
echo [1/3] Cleaning previous publish output...
|
||||
if exist "%OUTPUT%" (
|
||||
rmdir /s /q "%OUTPUT%"
|
||||
@@ -25,10 +52,6 @@ if exist "%OUTPUT%" (
|
||||
echo.
|
||||
|
||||
echo [2/3] Publishing...
|
||||
echo Project : %PROJECT%
|
||||
echo Output : %OUTPUT%
|
||||
echo RID : %RID%
|
||||
echo Framework: %FRAMEWORK%
|
||||
echo.
|
||||
|
||||
dotnet publish "%PROJECT%" ^
|
||||
@@ -50,36 +73,37 @@ echo [3/3] Verifying output...
|
||||
|
||||
set PASS=1
|
||||
|
||||
if not exist "%OUTPUT%\EnvelopeGenerator.Server.exe" (
|
||||
if not exist "!OUTPUT!\EnvelopeGenerator.Server.exe" (
|
||||
echo [FAIL] EnvelopeGenerator.Server.exe not found!
|
||||
set PASS=0
|
||||
)
|
||||
if not exist "%OUTPUT%\hostfxr.dll" (
|
||||
) else ( echo [OK] EnvelopeGenerator.Server.exe )
|
||||
if not exist "!OUTPUT!\hostfxr.dll" (
|
||||
echo [FAIL] hostfxr.dll not found! (Not a self-contained publish?)
|
||||
set PASS=0
|
||||
)
|
||||
if not exist "%OUTPUT%\coreclr.dll" (
|
||||
) else ( echo [OK] hostfxr.dll )
|
||||
if not exist "!OUTPUT!\coreclr.dll" (
|
||||
echo [FAIL] coreclr.dll not found! (Not a self-contained publish?)
|
||||
set PASS=0
|
||||
)
|
||||
if not exist "%OUTPUT%\Microsoft.Extensions.DependencyInjection.Abstractions.dll" (
|
||||
) else ( echo [OK] coreclr.dll )
|
||||
if not exist "!OUTPUT!\Microsoft.Extensions.DependencyInjection.Abstractions.dll" (
|
||||
echo [FAIL] Microsoft.Extensions.DependencyInjection.Abstractions.dll not found!
|
||||
set PASS=0
|
||||
)
|
||||
if not exist "%OUTPUT%\web.config" (
|
||||
) else ( echo [OK] Microsoft.Extensions.DependencyInjection.Abstractions.dll )
|
||||
if not exist "!OUTPUT!\web.config" (
|
||||
echo [FAIL] web.config not found!
|
||||
set PASS=0
|
||||
)
|
||||
) else ( echo [OK] web.config )
|
||||
|
||||
if "%PASS%"=="1" (
|
||||
if "!PASS!"=="1" (
|
||||
echo.
|
||||
echo ============================================================
|
||||
echo PUBLISH SUCCEEDED
|
||||
echo Output folder: %~dp0%OUTPUT%
|
||||
echo Version : !VERSION!
|
||||
echo Output folder: !OUTPUT!
|
||||
echo ============================================================
|
||||
echo.
|
||||
echo Next steps:
|
||||
echo 1. Copy the contents of '%OUTPUT%' to the IIS application directory
|
||||
echo 1. Copy the contents of the output folder to the IIS application directory
|
||||
echo 2. Set the IIS Application Pool to 'No Managed Code'
|
||||
echo 3. Recycle the Application Pool
|
||||
echo.
|
||||
|
||||
Reference in New Issue
Block a user