Compare commits
9 Commits
11c8886d6f
...
435be882c9
| Author | SHA1 | Date | |
|---|---|---|---|
| 435be882c9 | |||
| 09257d7766 | |||
| 16fa4b460f | |||
| f4aae8c787 | |||
| ed1468194d | |||
| f9f94c900e | |||
| 18df6221c9 | |||
| b15f95755c | |||
| 4be9e38a9e |
40
AGENTS.md
40
AGENTS.md
@@ -50,19 +50,6 @@ This section explains every markdown file in the repo: what it contains, why it
|
||||
|
||||
---
|
||||
|
||||
### Ticket / Feature Plans (Active Work Items)
|
||||
|
||||
#### `EnvelopeGenerator.Server/SF-74-PLAN.md`
|
||||
**What:** Implementation plan for SF-74 ticket — Theme selection, Dark Mode, Grid Layout Persistence, signFLOW logo, and customer logo area on the sender dashboard. Includes the original ticket requirements, component breakdown, and implementation steps.
|
||||
**Why it exists:** Captures the agreed scope and implementation decisions for SF-74 so work can be resumed or reviewed without re-reading the ticket system.
|
||||
**When you need it:**
|
||||
- Resuming work on theme, dark mode, or grid persistence features
|
||||
- Checking what was agreed for logo positioning and size
|
||||
- Verifying which features of SF-74 are complete vs. pending
|
||||
**Maintenance:** Mark items as complete as they are implemented. Archive when the ticket is closed.
|
||||
|
||||
---
|
||||
|
||||
### Deployment Docs
|
||||
|
||||
#### `EnvelopeGenerator.Server/EnvelopeGenerator.Server/README.md`
|
||||
@@ -76,33 +63,6 @@ This section explains every markdown file in the repo: what it contains, why it
|
||||
|
||||
---
|
||||
|
||||
### Fix Reports (Historical — Read-Only)
|
||||
|
||||
These files document completed bug fixes. They exist as audit trails and as references if a similar issue resurfaces. Do not modify them.
|
||||
|
||||
#### `fix-report-label-read-and-confirmed.md`
|
||||
**What:** Documents the fix for an incorrect label in the Signature Certificate report (`Signierungszertifikat`). For "Read and Sign" envelope type (`EnvelopeTypeId = 2`), the history event showed "Document signed" instead of "Read and confirmed" / "Gelesen und bestätigt".
|
||||
**Branch:** `fix/report-label-read-and-confirmed` — **COMPLETED**
|
||||
**When you need it:** If a similar report label regression appears, or when adding new envelope type-specific label logic to the certificate report.
|
||||
|
||||
#### `fix-signature-field-formatting.md`
|
||||
**What:** Documents a signature stamp layout fix triggered by WISAG customer feedback (email 3/7/2026). The finalized PDF's signature stamp had incorrect spacing: certificate hash too close to name, position text wrapping, no spacing before date. Fix was applied in the GdPicture14 PDF burning pipeline — not in the Blazor UI.
|
||||
**When you need it:** If signature stamp formatting regresses, or when modifying the PDF stamping/burning pipeline in the API or infrastructure layer.
|
||||
|
||||
---
|
||||
|
||||
### Service Migration Docs
|
||||
|
||||
#### `EnvelopeGenerator.Service/MIGRATION_PLAN.md`
|
||||
**What:** Migration plan (v3) for porting `EnvelopeGenerator.Service_legacy` (VB.NET Windows Service) to `EnvelopeGenerator.Service` (C# Worker Service, .NET Framework 4.6.2). Documents connection string handling, job scheduling (Quartz → IHostedService), and deep analysis of legacy code.
|
||||
**Why it exists:** The Windows Service handles background jobs (email dispatch, envelope finalization). This plan ensures the port is complete and no job logic is silently dropped.
|
||||
**When you need it:**
|
||||
- Modifying background jobs (email sending, PDF finalization, access code dispatch)
|
||||
- Debugging service startup or job execution issues
|
||||
- Checking if a specific legacy VB.NET job has been ported
|
||||
|
||||
---
|
||||
|
||||
## Active Architecture (Post-Migration)
|
||||
|
||||
**Frontend:** Blazor Auto (Server+WASM hybrid)
|
||||
|
||||
@@ -43,7 +43,10 @@
|
||||
</svg>
|
||||
}
|
||||
</div>
|
||||
<div class="sender-title">Umschlag-Übersicht</div>
|
||||
<div class="sender-title" aria-label="signFLOW Portal - Übersicht">
|
||||
<span class="sender-title-main">signFLOW Portal</span>
|
||||
<span class="sender-title-sub">Übersicht</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@* ── Kunden-Logo (Mitte) ───────────────────────────────────────── *@
|
||||
@@ -51,7 +54,7 @@
|
||||
@if (_companyLogo is not null && !string.IsNullOrEmpty(_companyLogo.Src))
|
||||
{
|
||||
<img src="@_companyLogo.Src"
|
||||
style="height: @_companyLogo.Height"
|
||||
style="--sf-company-logo-height: @_companyLogo.Height"
|
||||
class="@_companyLogo.GetClass("Show")"
|
||||
alt="Kunden-Logo" />
|
||||
}
|
||||
@@ -104,6 +107,40 @@
|
||||
Vorschau
|
||||
</button>
|
||||
|
||||
<div class="sender-export-group" title="Export der aktuellen Tabellenansicht">
|
||||
<select id="sender-export-format" class="sender-export-format" @bind="_selectedExportFormat" aria-label="Exportformat auswählen">
|
||||
<option value="xlsx">XLSX</option>
|
||||
<option value="csv">CSV</option>
|
||||
<option value="pdf">PDF</option>
|
||||
</select>
|
||||
|
||||
<label class="sender-export-selected">
|
||||
<input type="checkbox" @bind="_exportSelectedRowsOnly" />
|
||||
<span class="sender-export-selected-text">
|
||||
<span>Nur</span>
|
||||
<span>ausgewählte</span>
|
||||
</span>
|
||||
</label>
|
||||
|
||||
<button class="sender-btn sender-btn--export"
|
||||
@onclick="ExportSelectedFormatAsync"
|
||||
disabled="@IsExportDisabled()"
|
||||
title="Aktuelle Ansicht exportieren">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" viewBox="0 0 16 16">
|
||||
<path d="M.5 9.9a.5.5 0 0 1 .5.5V13a1 1 0 0 0 1 1h12a1 1 0 0 0 1-1v-2.6a.5.5 0 0 1 1 0V13a2 2 0 0 1-2 2H2a2 2 0 0 1-2-2v-2.6a.5.5 0 0 1 .5-.5z" />
|
||||
<path d="M5.354 6.146a.5.5 0 0 1 .707 0L7.5 7.586V.5a.5.5 0 0 1 1 0v7.086l1.439-1.44a.5.5 0 0 1 .707.708l-2.293 2.293a.5.5 0 0 1-.707 0L5.354 6.854a.5.5 0 0 1 0-.708z" />
|
||||
</svg>
|
||||
@if (_isExporting)
|
||||
{
|
||||
<span class="spinner-border spinner-border-sm" style="width: 14px; height: 14px;"></span>
|
||||
}
|
||||
else
|
||||
{
|
||||
<span>Exportieren</span>
|
||||
}
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<button class="sender-btn sender-btn--logout" @onclick="LogoutAsync" disabled="@_isLoggingOut" title="Abmelden">
|
||||
@if (_isLoggingOut)
|
||||
{
|
||||
@@ -171,19 +208,11 @@
|
||||
</div>
|
||||
|
||||
<div class="sender-grid-wrapper">
|
||||
|
||||
@* 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"
|
||||
KeyFieldName="Id"
|
||||
ShowFilterRow="true"
|
||||
ShowSearchBox="true"
|
||||
AllowColumnReorder="true"
|
||||
@@ -213,7 +242,7 @@
|
||||
<strong>@((cellContext.DataItem as EnvelopeDto)?.Title)</strong>
|
||||
</CellDisplayTemplate>
|
||||
</DxGridDataColumn>
|
||||
<DxGridDataColumn FieldName="Status" Caption="Status">
|
||||
<DxGridDataColumn FieldName="Status" Caption="Status" ExportEnabled="false">
|
||||
<CellDisplayTemplate Context="cellContext">
|
||||
@{
|
||||
var envelope = cellContext.DataItem as EnvelopeDto;
|
||||
@@ -228,7 +257,7 @@
|
||||
}
|
||||
</CellDisplayTemplate>
|
||||
</DxGridDataColumn>
|
||||
<DxGridDataColumn FieldName="EnvelopeReceivers" Caption="Empfänger">
|
||||
<DxGridDataColumn FieldName="EnvelopeReceivers" Caption="Empfänger" ExportEnabled="false">
|
||||
<CellDisplayTemplate Context="cellContext">
|
||||
@{
|
||||
var envelope = cellContext.DataItem as EnvelopeDto;
|
||||
@@ -308,6 +337,7 @@
|
||||
{
|
||||
<DxGrid Data="@_completedEnvelopes"
|
||||
@ref="_gridCompleted"
|
||||
KeyFieldName="Id"
|
||||
ShowFilterRow="true"
|
||||
ShowSearchBox="true"
|
||||
PageSize="20"
|
||||
@@ -334,7 +364,7 @@
|
||||
<strong>@((cellContext.DataItem as EnvelopeDto)?.Title)</strong>
|
||||
</CellDisplayTemplate>
|
||||
</DxGridDataColumn>
|
||||
<DxGridDataColumn FieldName="Status" Caption="Status">
|
||||
<DxGridDataColumn FieldName="Status" Caption="Status" ExportEnabled="false">
|
||||
<CellDisplayTemplate Context="cellContext">
|
||||
@{
|
||||
var envelope = cellContext.DataItem as EnvelopeDto;
|
||||
@@ -349,7 +379,7 @@
|
||||
}
|
||||
</CellDisplayTemplate>
|
||||
</DxGridDataColumn>
|
||||
<DxGridDataColumn FieldName="EnvelopeReceivers" Caption="Empfänger">
|
||||
<DxGridDataColumn FieldName="EnvelopeReceivers" Caption="Empfänger" ExportEnabled="false">
|
||||
<CellDisplayTemplate Context="cellContext">
|
||||
@{
|
||||
var envelope = cellContext.DataItem as EnvelopeDto;
|
||||
@@ -514,6 +544,11 @@
|
||||
private byte[]? _previewPdfBytes = null;
|
||||
private string? _previewErrorMessage = null;
|
||||
|
||||
// Phase 6e (step 1): UI state only. Export logic is added in step 2.
|
||||
private string _selectedExportFormat = "xlsx";
|
||||
private bool _exportSelectedRowsOnly = false;
|
||||
private bool _isExporting = false;
|
||||
|
||||
// Stable keys used to store/restore each grid's layout in UserPreferences.GridLayouts
|
||||
private const string GridKeyActive = "sender.active-envelopes";
|
||||
private const string GridKeyCompleted = "sender.completed-envelopes";
|
||||
@@ -572,6 +607,66 @@
|
||||
await LoadEnvelopesAsync();
|
||||
}
|
||||
|
||||
bool IsExportDisabled()
|
||||
{
|
||||
if (_isLoading || _isExporting)
|
||||
return true;
|
||||
|
||||
return _exportSelectedRowsOnly && _selectedEnvelope is null;
|
||||
}
|
||||
|
||||
async Task ExportSelectedFormatAsync()
|
||||
{
|
||||
var grid = _activeTab == "active" ? _gridActive : _gridCompleted;
|
||||
if (grid is null)
|
||||
return;
|
||||
|
||||
_isExporting = true;
|
||||
await InvokeAsync(StateHasChanged);
|
||||
|
||||
try
|
||||
{
|
||||
var tabToken = _activeTab == "completed" ? "abgeschlossen" : "aktiv";
|
||||
var stamp = DateTime.Now.ToString("yyyy-MM-dd");
|
||||
var exportBaseName = $"Umschlaege_{tabToken}_{stamp}";
|
||||
|
||||
switch (_selectedExportFormat)
|
||||
{
|
||||
case "csv":
|
||||
await grid.ExportToCsvAsync(
|
||||
exportBaseName,
|
||||
new GridCsvExportOptions
|
||||
{
|
||||
ExportSelectedRowsOnly = _exportSelectedRowsOnly
|
||||
});
|
||||
break;
|
||||
case "pdf":
|
||||
await grid.ExportToPdfAsync(
|
||||
exportBaseName,
|
||||
new GridPdfExportOptions
|
||||
{
|
||||
ExportSelectedRowsOnly = _exportSelectedRowsOnly,
|
||||
SelectedRowsExportMode = GridSelectedRowsExportMode.KeepGrouping
|
||||
});
|
||||
break;
|
||||
default:
|
||||
await grid.ExportToXlsxAsync(
|
||||
exportBaseName,
|
||||
new GridXlExportOptions
|
||||
{
|
||||
ExportSelectedRowsOnly = _exportSelectedRowsOnly,
|
||||
SelectedRowsExportMode = GridSelectedRowsExportMode.KeepGrouping
|
||||
});
|
||||
break;
|
||||
}
|
||||
}
|
||||
finally
|
||||
{
|
||||
_isExporting = false;
|
||||
await InvokeAsync(StateHasChanged);
|
||||
}
|
||||
}
|
||||
|
||||
void CreateEnvelope()
|
||||
{
|
||||
Navigation.NavigateTo("/sender/editor");
|
||||
@@ -753,4 +848,4 @@
|
||||
var json = JsonSerializer.Serialize(e.Layout);
|
||||
await UserPreferencesService.SaveGridLayoutAsync(GridKeyCompleted, json);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -22,13 +22,29 @@
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: 1.5rem;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.sender-title-section {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 1rem;
|
||||
gap: 0.75rem;
|
||||
flex-shrink: 0; /* don't shrink — logo area in the middle gets the space */
|
||||
min-width: 0;
|
||||
position: relative;
|
||||
z-index: 2;
|
||||
}
|
||||
|
||||
.sender-logo {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
flex: 0 0 auto;
|
||||
}
|
||||
|
||||
.sender-logo .signflow-app-logo {
|
||||
height: clamp(20px, 1.6vw, 24px) !important;
|
||||
width: auto;
|
||||
}
|
||||
|
||||
/* ── Customer logo area (middle of action bar) ──────────────────────────── */
|
||||
@@ -40,6 +56,13 @@
|
||||
min-width: 0; /* allow flex child to shrink below content size */
|
||||
}
|
||||
|
||||
.sender-customer-logo-area .signflow-company-logo {
|
||||
height: clamp(13px, 1.45vw, 18px);
|
||||
max-width: min(24vw, 190px);
|
||||
width: auto;
|
||||
object-fit: contain;
|
||||
}
|
||||
|
||||
/* Placeholder shown when no customer logo is configured */
|
||||
.sf-customer-logo-placeholder {
|
||||
display: flex;
|
||||
@@ -64,10 +87,29 @@
|
||||
}
|
||||
|
||||
.sender-title {
|
||||
font-size: 1.25rem;
|
||||
font-weight: 700;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: center;
|
||||
align-items: flex-start;
|
||||
gap: 0.08rem;
|
||||
line-height: 1.12;
|
||||
color: #1e293b;
|
||||
letter-spacing: -0.025em;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.sender-title-main {
|
||||
font-size: clamp(1rem, 1.25vw, 1.12rem);
|
||||
font-weight: 700;
|
||||
letter-spacing: -0.02em;
|
||||
white-space: normal;
|
||||
}
|
||||
|
||||
.sender-title-sub {
|
||||
color: #475569;
|
||||
font-size: clamp(0.72rem, 0.95vw, 0.78rem);
|
||||
font-weight: 600;
|
||||
letter-spacing: 0.01em;
|
||||
white-space: normal;
|
||||
}
|
||||
|
||||
.sender-toolbar {
|
||||
@@ -136,6 +178,58 @@
|
||||
min-width: 38px;
|
||||
}
|
||||
|
||||
.sender-btn--export {
|
||||
padding-inline: 0.875rem;
|
||||
}
|
||||
|
||||
.sender-export-group {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 0.45rem;
|
||||
padding: 0.35rem 0.5rem;
|
||||
border: 1px solid rgba(126, 34, 206, 0.18);
|
||||
border-radius: 10px;
|
||||
background: rgba(126, 34, 206, 0.04);
|
||||
}
|
||||
|
||||
.sender-export-format {
|
||||
height: 32px;
|
||||
min-width: 84px;
|
||||
padding: 0 0.5rem;
|
||||
border: 1px solid rgba(126, 34, 206, 0.25);
|
||||
border-radius: 8px;
|
||||
background: #fff;
|
||||
color: #1e293b;
|
||||
font-size: 0.8rem;
|
||||
font-weight: 600;
|
||||
outline: none;
|
||||
}
|
||||
|
||||
.sender-export-format:focus {
|
||||
border-color: rgba(126, 34, 206, 0.55);
|
||||
box-shadow: 0 0 0 2px rgba(126, 34, 206, 0.12);
|
||||
}
|
||||
|
||||
.sender-export-selected {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 0.35rem;
|
||||
font-size: 0.74rem;
|
||||
color: #4b5563;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.sender-export-selected-text {
|
||||
display: inline-flex;
|
||||
flex-direction: column;
|
||||
justify-content: center;
|
||||
line-height: 1.05;
|
||||
}
|
||||
|
||||
.sender-export-selected input[type="checkbox"] {
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.sender-content {
|
||||
flex: 1;
|
||||
min-height: 0;
|
||||
@@ -199,20 +293,7 @@
|
||||
}
|
||||
|
||||
.sender-grid-wrapper {
|
||||
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;
|
||||
padding: 1rem 2rem 2rem;
|
||||
}
|
||||
|
||||
/* Hide DevExpress empty cells */
|
||||
@@ -300,13 +381,62 @@
|
||||
color: #c62828;
|
||||
}
|
||||
|
||||
@@media (max-width: 768px) {
|
||||
@media (max-width: 1320px) {
|
||||
.sender-action-bar__inner {
|
||||
flex-wrap: wrap;
|
||||
row-gap: 0.75rem;
|
||||
}
|
||||
|
||||
.sender-customer-logo-area {
|
||||
order: 2;
|
||||
flex: 1 1 100%;
|
||||
justify-content: flex-start;
|
||||
padding-left: 2.4rem;
|
||||
margin-top: 0;
|
||||
}
|
||||
|
||||
.sender-toolbar {
|
||||
order: 3;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.sender-title-main {
|
||||
font-size: 1.02rem;
|
||||
}
|
||||
|
||||
.sender-title-sub {
|
||||
font-size: 0.72rem;
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 768px) {
|
||||
.sender-action-bar {
|
||||
padding: 1rem 1.25rem;
|
||||
}
|
||||
|
||||
.sender-action-bar__inner {
|
||||
flex-wrap: wrap;
|
||||
gap: 0.9rem;
|
||||
}
|
||||
|
||||
.sender-title-section {
|
||||
flex: 1 1 auto;
|
||||
min-width: 220px;
|
||||
}
|
||||
|
||||
.sender-logo .signflow-app-logo {
|
||||
height: 20px !important;
|
||||
}
|
||||
|
||||
.sender-customer-logo-area .signflow-company-logo {
|
||||
height: 13px;
|
||||
max-width: 150px;
|
||||
}
|
||||
|
||||
.sender-customer-logo-area {
|
||||
padding-left: 0;
|
||||
justify-content: center;
|
||||
margin-top: 0;
|
||||
}
|
||||
|
||||
.sender-toolbar {
|
||||
@@ -314,8 +444,25 @@
|
||||
justify-content: flex-start;
|
||||
}
|
||||
|
||||
.sender-export-group {
|
||||
order: 98;
|
||||
}
|
||||
|
||||
.sender-btn--logout {
|
||||
order: 99;
|
||||
}
|
||||
|
||||
.sender-title {
|
||||
font-size: 1.125rem;
|
||||
margin-top: 0;
|
||||
gap: 0;
|
||||
}
|
||||
|
||||
.sender-title-main {
|
||||
font-size: 0.94rem;
|
||||
}
|
||||
|
||||
.sender-title-sub {
|
||||
font-size: 0.68rem;
|
||||
}
|
||||
|
||||
.sender-content {
|
||||
@@ -337,6 +484,18 @@
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 560px) {
|
||||
.sender-title-section {
|
||||
flex-direction: column;
|
||||
align-items: flex-start;
|
||||
gap: 0.4rem;
|
||||
}
|
||||
|
||||
.sender-title {
|
||||
padding-left: 0.1rem;
|
||||
}
|
||||
}
|
||||
|
||||
/* ── Theme overrides ─────────────────────────────────────────────────────── */
|
||||
/* Selector: [data-sf-theme="<name>"] set on <html> by signflow-theme.js */
|
||||
|
||||
@@ -373,6 +532,20 @@
|
||||
box-shadow: 0 4px 12px rgba(147, 51, 234, 0.22);
|
||||
}
|
||||
|
||||
[data-sf-theme="purple"] .sender-export-group {
|
||||
border-color: rgba(147, 51, 234, 0.28);
|
||||
background: rgba(147, 51, 234, 0.08);
|
||||
}
|
||||
|
||||
[data-sf-theme="purple"] .sender-export-format {
|
||||
border-color: rgba(147, 51, 234, 0.36);
|
||||
}
|
||||
|
||||
[data-sf-theme="purple"] .sender-export-format:focus {
|
||||
border-color: rgba(147, 51, 234, 0.6);
|
||||
box-shadow: 0 0 0 2px rgba(147, 51, 234, 0.14);
|
||||
}
|
||||
|
||||
[data-sf-theme="purple"] .sender-tabs {
|
||||
border-bottom-color: rgba(147, 51, 234, 0.12);
|
||||
}
|
||||
@@ -382,6 +555,10 @@
|
||||
border-bottom-color: #9333ea;
|
||||
}
|
||||
|
||||
[data-sf-theme="purple"] .sender-title-sub {
|
||||
color: #6b21a8;
|
||||
}
|
||||
|
||||
/* ── Blazing Dark theme ────────────────────────────────────────────────────
|
||||
Dark navy background, frosted glass surfaces turn semi-dark */
|
||||
[data-sf-theme="blazing-dark"] .sender-dashboard-layout {
|
||||
@@ -398,6 +575,10 @@
|
||||
color: rgba(255, 255, 255, 0.92);
|
||||
}
|
||||
|
||||
[data-sf-theme="blazing-dark"] .sender-title-sub {
|
||||
color: rgba(224, 231, 255, 0.82);
|
||||
}
|
||||
|
||||
[data-sf-theme="blazing-dark"] .sender-logo svg {
|
||||
color: #818cf8;
|
||||
filter: drop-shadow(0 2px 4px rgba(129, 140, 248, 0.3));
|
||||
@@ -445,6 +626,27 @@
|
||||
border-color: transparent;
|
||||
}
|
||||
|
||||
[data-sf-theme="blazing-dark"] .sender-export-group {
|
||||
border-color: rgba(129, 140, 248, 0.26);
|
||||
background: rgba(99, 102, 241, 0.08);
|
||||
}
|
||||
|
||||
[data-sf-theme="blazing-dark"] .sender-export-label,
|
||||
[data-sf-theme="blazing-dark"] .sender-export-selected {
|
||||
color: rgba(255, 255, 255, 0.72);
|
||||
}
|
||||
|
||||
[data-sf-theme="blazing-dark"] .sender-export-format {
|
||||
background: rgba(255, 255, 255, 0.08);
|
||||
color: rgba(255, 255, 255, 0.9);
|
||||
border-color: rgba(129, 140, 248, 0.35);
|
||||
}
|
||||
|
||||
[data-sf-theme="blazing-dark"] .sender-export-format:focus {
|
||||
border-color: rgba(129, 140, 248, 0.58);
|
||||
box-shadow: 0 0 0 2px rgba(129, 140, 248, 0.16);
|
||||
}
|
||||
|
||||
[data-sf-theme="blazing-dark"] .sender-grid-container {
|
||||
background: rgba(15, 20, 40, 0.95);
|
||||
box-shadow: 0 25px 50px -12px rgba(0, 0, 0, 0.5), 0 0 0 1px rgba(255, 255, 255, 0.06);
|
||||
|
||||
@@ -1,418 +0,0 @@
|
||||
# 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, 6a–6c)
|
||||
|
||||
| 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]`) | ✅ |
|
||||
| 6a | signFLOW-Logo (CustomImages Options, Controller, Service, Dateien) | ✅ |
|
||||
| 6b | Kunden-Logo Placeholder (sender-customer-logo-area, CSS) | ✅ |
|
||||
| 6c | Grid-Spalten (EnvelopeTypeTitle, AddedWhen, ChangedWhen) + Persistence Hint | ✅ |
|
||||
|
||||
**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,315 +0,0 @@
|
||||
# Migration Plan: EnvelopeGenerator.Service_legacy ? EnvelopeGenerator.Service
|
||||
**Revision v3** — Full deep analysis complete
|
||||
|
||||
## Overview
|
||||
|
||||
Migrate the legacy VB.NET Windows Service (`EnvelopeGenerator.Service_legacy`) to a modern
|
||||
C# Worker Service (`EnvelopeGenerator.Service`) targeting **.NET Framework 4.6.2**, using
|
||||
`Microsoft.Extensions.Hosting`, `IConfiguration`, `ILogger<T>` and **NLog**.
|
||||
|
||||
---
|
||||
|
||||
## Deep Analysis Findings (vs. v1/v2)
|
||||
|
||||
### Finding 1 — Connection String: Plain Text, No Encryption Required
|
||||
`FinalizeDocumentJob` and `APIEnvelopeJob` call `MSSQLServer.DecryptConnectionString()` on the
|
||||
`JobDataMap[Value.DATABASE]` value. However, `MSSQLServer.DecryptConnectionString()` is a pass-through
|
||||
for plain-text strings — no wrapping/encryption step is needed.
|
||||
|
||||
**Resolution**: The connection string is read from `IConfiguration` via the standard
|
||||
`ConnectionStrings:Default` key (same key as `appsettings.Database.json` in `EnvelopeGenerator.Web`).
|
||||
It is passed **as-is** to both the Worker-level `MSSQLServer` and the `JobDataMap`. No
|
||||
`EncryptConnectionString()` call is made anywhere in the new service.
|
||||
|
||||
### Finding 2 — Dual TempFiles Architecture
|
||||
There are **two independent** `TempFiles` classes:
|
||||
- `EnvelopeGenerator.CommonServices.TempFiles` — used **inside each job** via `New TempFiles(LogConfig)`.
|
||||
This is called on every job execution and manages the job's own temp lifecycle. It expects `LogConfig`.
|
||||
**We do not touch this.**
|
||||
- `EnvelopeGenerator.Service_legacy.TempFiles` — used at the **service/host level** (startup cleanup,
|
||||
shutdown cleanup). This is the one we rewrite in C# in the Service project using `ILogger<TempFiles>`.
|
||||
|
||||
### Finding 3 — LogConfig is a Hard Dependency of All Jobs
|
||||
`FinalizeDocumentJob.Execute()` and `APIEnvelopeJob.Execute()` both do:
|
||||
```vb
|
||||
LogConfig = pContext.MergedJobDataMap.Item(Value.LOGCONFIG)
|
||||
Logger = LogConfig.GetLogger()
|
||||
myTempFiles = New TempFiles(LogConfig) ' ? CommonServices.TempFiles
|
||||
```
|
||||
`LogConfig` is **not optional**. The schedulers must construct a valid `LogConfig` and place it in the
|
||||
`JobDataMap`. It must carry the correct log path, debug flag, and application identity.
|
||||
|
||||
**Resolution**: A `LogConfigFactory` static helper class will be created in the Service project to
|
||||
build a `LogConfig` from `ServiceConfig` values:
|
||||
- `LogPath` = `Path.Combine(AppDomain.CurrentDomain.BaseDirectory, "Log")` (mirrors legacy `My.Application.Info.DirectoryPath + "\Log"`)
|
||||
- `Debug` = `ServiceConfig.Debug`
|
||||
- Application name = `"EnvelopeGenerator.Service"`
|
||||
|
||||
### Finding 4 — ProjectInstaller is Obsolete
|
||||
`ProjectInstaller.vb` was the old `installutil.exe` mechanism for Windows Service registration.
|
||||
With `AddWindowsService()` + `sc create` / PowerShell `New-Service`, this is **completely replaced**.
|
||||
No equivalent is needed in the new project.
|
||||
|
||||
### Finding 5 — GDPicture License SQL
|
||||
The legacy service queries: `SELECT LICENSE FROM TBDD_3RD_PARTY_MODULES WHERE NAME = 'GDPICTURE' and ACTIVE = 1`
|
||||
This must be replicated exactly in the Worker startup sequence. If the result is null/empty, startup fails
|
||||
with a descriptive exception (matching legacy behavior).
|
||||
|
||||
### Finding 6 — Quartz Version Mismatch
|
||||
`CommonServices` uses `Quartz 3.8.0`. `Service_legacy` uses `Quartz 3.15.0`.
|
||||
The new `EnvelopeGenerator.Service` must use **Quartz 3.8.0** to match `CommonServices` (same AppDomain,
|
||||
same version must be loaded). The legacy version bump in `Service_legacy` was inconsistent.
|
||||
|
||||
---
|
||||
|
||||
## 1. Project File (`EnvelopeGenerator.Service.csproj`)
|
||||
|
||||
- Add **NuGet packages**:
|
||||
- `NLog` (5.x, matching CommonServices' `NLog.5.0.5`)
|
||||
- `NLog.Extensions.Logging` (latest net462-compatible)
|
||||
- `Quartz` (**3.8.0** — must match CommonServices)
|
||||
- `Microsoft.Extensions.Hosting.WindowsServices` (for `AddWindowsService()`)
|
||||
- Add **project reference** to `EnvelopeGenerator.CommonServices` (vbproj)
|
||||
- Add **project reference** to `EnvelopeGenerator.Domain` (csproj)
|
||||
|
||||
---
|
||||
|
||||
## 2. Configuration (`ServiceConfig.cs`)
|
||||
|
||||
```csharp
|
||||
public class ServiceConfig
|
||||
{
|
||||
public string ConnectionString { get; set; } = string.Empty;
|
||||
public bool Debug { get; set; } = false;
|
||||
public int IntervalInMin { get; set; } = 1;
|
||||
public PDFBurnerParams PDFBurnerParams { get; set; } = new PDFBurnerParams();
|
||||
}
|
||||
```
|
||||
|
||||
Connection string is read via the standard `ConnectionStrings:Default` key — consistent with
|
||||
`appsettings.Database.json` used by `EnvelopeGenerator.Web`:
|
||||
|
||||
`appsettings.json`:
|
||||
```json
|
||||
{
|
||||
"ConnectionStrings": {
|
||||
"Default": ""
|
||||
},
|
||||
"ServiceConfig": {
|
||||
"Debug": false,
|
||||
"IntervalInMin": 1,
|
||||
"PDFBurnerParams": {
|
||||
"IgnoredLabels": [ "Date", "Datum", "ZIP", "PLZ", "Place", "Ort", "Position", "Stellung" ],
|
||||
"TopMargin": 0.1,
|
||||
"YOffset": -0.3,
|
||||
"FontName": "Arial",
|
||||
"FontSize": 8,
|
||||
"FontStyle": 2,
|
||||
"TruncationSuffix": "..",
|
||||
"TextMaxWidths": {
|
||||
"position": 1.41,
|
||||
"city": 1.41,
|
||||
"date": 2.0
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
> `FontStyle` integer: Regular=0, Bold=1, **Italic=2**, Underline=4, Strikeout=8.
|
||||
> All values mirror `PDFBurnerParams.vb` defaults exactly.
|
||||
|
||||
---
|
||||
|
||||
## 3. NLog Setup
|
||||
|
||||
**`nlog.config`**: File target ? `${basedir}/Log/service-${shortdate}.log`, async wrapper,
|
||||
layout with timestamp + level + logger + message + exception.
|
||||
|
||||
**`Program.cs`**: Wire via `NLog.Extensions.Logging`:
|
||||
```csharp
|
||||
builder.Logging.ClearProviders();
|
||||
builder.Logging.AddNLog("nlog.config");
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 4. LogConfigFactory (`LogConfigFactory.cs`)
|
||||
|
||||
New static helper — bridges `ServiceConfig` ? `LogConfig` (required by CommonServices jobs):
|
||||
|
||||
```csharp
|
||||
internal static class LogConfigFactory
|
||||
{
|
||||
public static LogConfig Create(ServiceConfig config)
|
||||
{
|
||||
var logPath = Path.Combine(AppDomain.CurrentDomain.BaseDirectory, "Log");
|
||||
var logConfig = new LogConfig(
|
||||
LogConfig.PathType.CustomPath, logPath, null,
|
||||
"Digital Data", "EnvelopeGenerator.Service");
|
||||
logConfig.Debug = config.Debug;
|
||||
return logConfig;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
This `LogConfig` instance is what gets placed into every `JobDataMap[Value.LOGCONFIG]`.
|
||||
|
||||
---
|
||||
|
||||
## 5. Quartz LogProvider (`QuartzLogProvider.cs`)
|
||||
|
||||
C# rewrite of `LogProvider.vb` — bridges Quartz `ILogProvider` ? `ILogger<T>`:
|
||||
|
||||
```csharp
|
||||
internal class QuartzLogProvider : ILogProvider
|
||||
{
|
||||
private readonly ILogger _logger;
|
||||
public QuartzLogProvider(ILogger logger) => _logger = logger;
|
||||
public Logger GetLogger(string name) => (level, func, ex, parameters) => { ... };
|
||||
// OpenNestedContext / OpenMappedContext ? NotImplementedException (same as legacy)
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 6. TempFiles (`TempFiles.cs`) — Service-level only
|
||||
|
||||
Service-level rewrite of `Service_legacy/TempFiles.vb` using `ILogger<TempFiles>`:
|
||||
- `Create()` ? create `%TEMP%\EnvelopeGenerator`, or clean existing files
|
||||
- `CleanUp()` ? delete the directory on service stop
|
||||
|
||||
> **Does NOT replace** `CommonServices.TempFiles` — that one continues to be used
|
||||
> inside each job via `LogConfig`.
|
||||
|
||||
---
|
||||
|
||||
## 7. Schedulers
|
||||
|
||||
### `Schedulers/SchedulerFinishEnvelope.cs`
|
||||
|
||||
```csharp
|
||||
public class SchedulerFinishEnvelope
|
||||
{
|
||||
// Constructor: ILogger<SchedulerFinishEnvelope>, IOptions<ServiceConfig>
|
||||
// Start(licenseKey, logConfig):
|
||||
// - Build JobDataMap:
|
||||
// [GDPICTURE] = licenseKey
|
||||
// [LOGCONFIG] = logConfig ? LogConfigFactory output
|
||||
// [DATABASE] = MSSQLServer.EncryptConnectionString(connectionString)
|
||||
// [PDF_BURNER_PARAMS] = config.PDFBurnerParams
|
||||
// - Schedule FinalizeDocumentJob with Quartz (interval from config)
|
||||
// - Register QuartzLogProvider
|
||||
// Stop(): Scheduler.Shutdown()
|
||||
}
|
||||
```
|
||||
|
||||
### `Schedulers/SchedulerEnvelopeTaskApi.cs`
|
||||
|
||||
```csharp
|
||||
public class SchedulerEnvelopeTaskApi
|
||||
{
|
||||
// Constructor: ILogger<SchedulerEnvelopeTaskApi>, IOptions<ServiceConfig>
|
||||
// Start(logConfig):
|
||||
// - Build JobDataMap:
|
||||
// [LOGCONFIG] = logConfig
|
||||
// [DATABASE] = MSSQLServer.EncryptConnectionString(connectionString)
|
||||
// - Schedule APIEnvelopeJob with Quartz (interval from config)
|
||||
// - Register QuartzLogProvider
|
||||
// Stop(): Scheduler.Shutdown()
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 8. Worker (`Worker.cs`)
|
||||
|
||||
Full lifecycle replacing the placeholder:
|
||||
|
||||
```
|
||||
ExecuteAsync:
|
||||
1. Read ServiceConfig from IOptions<ServiceConfig>
|
||||
2. Validate ConnectionString ? throw if empty
|
||||
3. Build LogConfig via LogConfigFactory
|
||||
4. Connect to DB (plain connection string — Worker-level only)
|
||||
5. Query: SELECT LICENSE FROM TBDD_3RD_PARTY_MODULES WHERE NAME='GDPICTURE' AND ACTIVE=1
|
||||
? throw if null/empty (matches legacy behavior)
|
||||
6. TempFiles.Create()
|
||||
7. await SchedulerFinishEnvelope.Start(licenseKey, logConfig)
|
||||
8. await Task.Delay(2500, stoppingToken) ? preserved from legacy
|
||||
9. await SchedulerEnvelopeTaskApi.Start(logConfig)
|
||||
10. await Task.Delay(Timeout.Infinite, stoppingToken) ? wait for cancellation
|
||||
|
||||
StopAsync override:
|
||||
11. await SchedulerFinishEnvelope.Stop()
|
||||
12. await SchedulerEnvelopeTaskApi.Stop()
|
||||
13. TempFiles.CleanUp()
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 9. Program.cs
|
||||
|
||||
```csharp
|
||||
var builder = Host.CreateApplicationBuilder(args);
|
||||
|
||||
builder.Logging.ClearProviders();
|
||||
builder.Logging.AddNLog("nlog.config");
|
||||
|
||||
builder.Services.Configure<ServiceConfig>(
|
||||
builder.Configuration.GetSection("ServiceConfig"));
|
||||
|
||||
builder.Services.AddSingleton<TempFiles>();
|
||||
builder.Services.AddSingleton<SchedulerFinishEnvelope>();
|
||||
builder.Services.AddSingleton<SchedulerEnvelopeTaskApi>();
|
||||
builder.Services.AddHostedService<Worker>();
|
||||
builder.Services.AddWindowsService(o => o.ServiceName = "EnvelopeGenerator.Service");
|
||||
|
||||
var host = builder.Build();
|
||||
host.Run();
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 10. Files to Create / Modify
|
||||
|
||||
| File | Action | Notes |
|
||||
|------|--------|-------|
|
||||
| `EnvelopeGenerator.Service.csproj` | **Modify** | Add packages, project refs |
|
||||
| `Program.cs` | **Modify** | Full DI/NLog/WindowsService setup |
|
||||
| `Worker.cs` | **Modify** | Full service lifecycle |
|
||||
| `ServiceConfig.cs` | **Create** | |
|
||||
| `LogConfigFactory.cs` | **Create** | Critical bridge: ServiceConfig ? LogConfig |
|
||||
| `TempFiles.cs` | **Create** | Service-level only; CommonServices.TempFiles untouched |
|
||||
| `QuartzLogProvider.cs` | **Create** | |
|
||||
| `Schedulers/SchedulerFinishEnvelope.cs` | **Create** | |
|
||||
| `Schedulers/SchedulerEnvelopeTaskApi.cs` | **Create** | |
|
||||
| `appsettings.json` | **Create** | All PDFBurnerParams explicit |
|
||||
| `nlog.config` | **Create** | |
|
||||
|
||||
**Not created** (obsolete in new architecture):
|
||||
- ~~`ProjectInstaller`~~ ? replaced by `sc create` / `New-Service`
|
||||
- ~~`Service.Designer.vb`~~ ? BackgroundService handles this
|
||||
- ~~`Service.resx`~~ ? not applicable
|
||||
|
||||
---
|
||||
|
||||
## 11. Key Design Decisions
|
||||
|
||||
| Decision | Rationale |
|
||||
|----------|-----------|
|
||||
| **Quartz 3.8.0** (not 3.15.0) | Must match CommonServices; same AppDomain, one version |
|
||||
| **Connection string plain-text, no encryption** | `MSSQLServer.DecryptConnectionString()` is a pass-through for plain strings; `ConnectionStrings:Default` key used (same convention as Web project) |
|
||||
| **Connection string in `ConnectionStrings:Default`** | Consistent with rest of the solution (`appsettings.Database.json`); `ServiceConfig` holds only service-specific settings |
|
||||
| **LogConfigFactory** | Jobs have hard dependency on `LogConfig` in JobDataMap; this is the clean bridge without leaking LogConfig into the DI container |
|
||||
| **2500ms delay preserved** | Legacy behavior; ensures Scheduler1 is fully initialized before Scheduler2 starts |
|
||||
| **`AddWindowsService()`** | Native SCM integration; replaces ProjectInstaller/installutil entirely |
|
||||
| **NLog log path = BaseDirectory/Log** | Mirrors `My.Application.Info.DirectoryPath + "\Log"` from legacy |
|
||||
| **CommonServices.TempFiles untouched** | Jobs create their own TempFiles internally via LogConfig; service-level TempFiles is separate concern |
|
||||
|
||||
---
|
||||
|
||||
## Approval Required
|
||||
|
||||
> Please review this plan and confirm before implementation begins.
|
||||
|
||||
@@ -20,8 +20,6 @@ EndProject
|
||||
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "src", "src", "{134D4164-B291-4E19-99B9-E4FA3AFAB62C}"
|
||||
ProjectSection(SolutionItems) = preProject
|
||||
COPILOT_CONTEXT.md = COPILOT_CONTEXT.md
|
||||
fix-report-label-read-and-confirmed.md = fix-report-label-read-and-confirmed.md
|
||||
fix-signature-field-formatting.md = fix-signature-field-formatting.md
|
||||
FORM_APPLICATION_CONTEXT.md = FORM_APPLICATION_CONTEXT.md
|
||||
RECEIVER_PDF_VIEWER_CONTEXT.md = RECEIVER_PDF_VIEWER_CONTEXT.md
|
||||
EndProjectSection
|
||||
@@ -45,7 +43,6 @@ EndProject
|
||||
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "EnvelopeGenerator.Server", "EnvelopeGenerator.Server", "{BF1700D5-592E-4FFA-84E8-5480E289A1F0}"
|
||||
ProjectSection(SolutionItems) = preProject
|
||||
EnvelopeGenerator.Server\publish.bat = EnvelopeGenerator.Server\publish.bat
|
||||
EnvelopeGenerator.Server\SF-74-PLAN.md = EnvelopeGenerator.Server\SF-74-PLAN.md
|
||||
EndProjectSection
|
||||
EndProject
|
||||
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "EnvelopeGenerator.Server", "EnvelopeGenerator.Server\EnvelopeGenerator.Server\EnvelopeGenerator.Server.csproj", "{4E6C54DA-576D-0955-2564-9EC890BB8279}"
|
||||
|
||||
@@ -1,135 +0,0 @@
|
||||
# Fix: Report Label "Read and confirmed" for Read and Sign Envelopes
|
||||
|
||||
## Status
|
||||
✅ **COMPLETED**
|
||||
|
||||
## Git Branch
|
||||
```
|
||||
fix/report-label-read-and-confirmed
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Problem
|
||||
|
||||
In the **Signature Certificate report** (`Signierungszertifikat`), the history event
|
||||
list always showed **"Document signed"** for status `DocumentSigned` (code `2005`) —
|
||||
even when the envelope type was **"Read and Sign"** (`EnvelopeTypeId = 2`).
|
||||
|
||||
For "Read and Sign" envelopes the correct label must be **"Read and confirmed"**
|
||||
(DE: **"Gelesen und bestätigt"**).
|
||||
|
||||
### Screenshot reference
|
||||
The report shows a table with columns *Ereignis | Benutzer | Zeitstempel*.
|
||||
The row with status 2005 displayed "Document signed" where "Read and confirmed"
|
||||
was expected.
|
||||
|
||||
---
|
||||
|
||||
## Root Cause
|
||||
|
||||
`ReportItem.ItemStatusTranslated` in
|
||||
`EnvelopeGenerator.CommonServices\Jobs\FinalizeDocument\ReportItem.vb`
|
||||
resolved the display label by calling:
|
||||
|
||||
```vb
|
||||
My.Resources.Model.ResourceManager.GetString(ItemStatus.ToString())
|
||||
```
|
||||
|
||||
There was **no branching logic** to handle the Read and Sign envelope type.
|
||||
The same `DocumentSigned` key was always used regardless of envelope type.
|
||||
|
||||
---
|
||||
|
||||
## Solution
|
||||
|
||||
### Convention followed
|
||||
The project already uses a `RaC` (**R**ead **a**nd **C**onfirm) suffix for
|
||||
Read-and-Sign label variants across all resource files:
|
||||
|
||||
| Existing RaC keys |
|
||||
|---|
|
||||
| `DocumentRejectedRaC` |
|
||||
| `EnvelopeReportCreatedRaC` |
|
||||
| `MessageConfirmationSentRaC` |
|
||||
|
||||
### New resource key added: `DocumentSignedRaC`
|
||||
|
||||
| Resource file | Value |
|
||||
|---|---|
|
||||
| `Model.resx` (DE, default) | `Gelesen und bestätigt` |
|
||||
| `Model.en.resx` (EN) | `Read and confirmed` |
|
||||
|
||||
### Logic change in `ReportItem.vb`
|
||||
|
||||
```vb
|
||||
Public ReadOnly Property ItemStatusTranslated As String
|
||||
Get
|
||||
Dim oStatusKey = ItemStatus.ToString()
|
||||
|
||||
If ItemStatus = EnvelopeStatus.DocumentSigned AndAlso
|
||||
Envelope IsNot Nothing AndAlso
|
||||
Envelope.IsReadAndConfirm() Then
|
||||
oStatusKey = "DocumentSignedRaC"
|
||||
End If
|
||||
|
||||
Return My.Resources.Model.ResourceManager.GetString(oStatusKey)
|
||||
End Get
|
||||
End Property
|
||||
```
|
||||
|
||||
`Envelope.IsReadAndConfirm()` is defined in
|
||||
`EnvelopeGenerator.Domain\Interfaces\IEnvelope.cs` and returns `true`
|
||||
when `EnvelopeTypeId == 2`.
|
||||
|
||||
---
|
||||
|
||||
## Files Changed
|
||||
|
||||
| # | File | Change | Status |
|
||||
|---|---|---|---|
|
||||
| 1 | `EnvelopeGenerator.CommonServices\Jobs\FinalizeDocument\ReportItem.vb` | Added `RaC` branching logic to `ItemStatusTranslated` | ? Done |
|
||||
| 2 | `EnvelopeGenerator.Domain\Resources\Model.resx` | Added `DocumentSignedRaC` = `Gelesen und bestätigt` | ✅ Done |
|
||||
| 3 | `EnvelopeGenerator.CommonServices\Strings\Model.resx` | Added `DocumentSignedRaC` = `Gelesen und bestätigt` | ✅ Done |
|
||||
| 4 | `EnvelopeGenerator.CommonServices\Strings\Model.en.resx` | Added `DocumentSignedRaC` = `Read and confirmed` | ✅ Done |
|
||||
| 5 | `EnvelopeGenerator.Domain\Resources\Model.Designer.cs` | Added `DocumentSignedRaC` strongly-typed property | ✅ Done |
|
||||
|
||||
---
|
||||
|
||||
## How to resume (for next agent)
|
||||
|
||||
All pending items are **resource file additions only**.
|
||||
Each file needs one new `<data>` block inserted **directly after** the existing
|
||||
`DocumentSigned` entry:
|
||||
|
||||
```xml
|
||||
<data name="DocumentSignedRaC" xml:space="preserve">
|
||||
<value>Gelesen und bestätigt</value> <!-- DE files -->
|
||||
<!-- OR -->
|
||||
<value>Read and confirmed</value> <!-- EN file -->
|
||||
</data>
|
||||
```
|
||||
|
||||
`Model.Designer.cs` also needs a new strongly-typed property added after
|
||||
`DocumentSigned`:
|
||||
|
||||
```csharp
|
||||
/// <summary>
|
||||
/// Looks up a localized string similar to Gelesen und bestätigt.
|
||||
/// </summary>
|
||||
public static string DocumentSignedRaC {
|
||||
get {
|
||||
return ResourceManager.GetString("DocumentSignedRaC", resourceCulture);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Testing
|
||||
|
||||
After changes, generate a Signature Certificate report for a **Read and Sign**
|
||||
envelope and verify:
|
||||
- History row with status 2005 shows **"Read and confirmed"** (EN)
|
||||
- History row with status 2005 shows **"Gelesen und bestätigt"** (DE)
|
||||
- Regular (non-RaC) envelopes still show **"Document signed"** / **"Dokument unterzeichnet"**
|
||||
@@ -1,111 +0,0 @@
|
||||
# Signature Field Formatting — WISAG Feedback
|
||||
|
||||
## Context
|
||||
|
||||
Customer feedback received from the new WISAG signFLOW project lead (email dated 3/7/2026).
|
||||
The signature stamp rendered inside the finalized PDF has layout issues that need to be addressed.
|
||||
|
||||
### Current State (screenshot from customer email)
|
||||
|
||||
```
|
||||
Signiert von ___________
|
||||
[ signature image ]
|
||||
Armin Schneider
|
||||
C6B85E96A520C60... ← certificate hash, too close to name
|
||||
Bereichsleiter Kern- ← position text wraps, no spacing to date
|
||||
Anwendungen
|
||||
Frankfurt am Main
|
||||
23.06.2026, 17:13:42 GMT+2
|
||||
```
|
||||
|
||||
### Issues Reported
|
||||
|
||||
1. **Spacing between name and certificate number** is too small — needs more vertical gap.
|
||||
2. **Spacing between position/title and date/time** is too small — should be dynamic (expand with content).
|
||||
3. *(Secondary, out of scope here)* Multiple signature fields side-by-side scale individually, causing uneven heights.
|
||||
|
||||
---
|
||||
|
||||
## Agreed Solution
|
||||
|
||||
> **"Height stays fixed. Text stretches horizontally. Beyond a certain character limit, truncate with '...'"**
|
||||
> *(Original: "Höhe bleibt fix. Text dehnt sich horizontal aus. Ab einem bestimmten Limit wird mit '...' abgekürzt.")*
|
||||
|
||||
### Rationale
|
||||
|
||||
- The signature field dimensions are **fixed** (`1.77" × 1.96"`). Allowing text to wrap causes position and date rows to collide vertically.
|
||||
- Truncating long strings with ellipsis (`...`) keeps the layout deterministic and prevents overflow.
|
||||
- Per-field max-length limits allow tuning each text row independently (name, certificate, position, city, date).
|
||||
|
||||
---
|
||||
|
||||
## Implementation Plan
|
||||
|
||||
### 1. `PDFBurnerParams.vb` — Add per-field max-length dictionary
|
||||
|
||||
Add a new property `TextMaxLengths` that maps each field name (matching `EGName` keys) to a maximum character count.
|
||||
|
||||
```vbnet
|
||||
' EnvelopeGenerator.CommonServices/Jobs/FinalizeDocument/PDFBurnerParams.vb
|
||||
|
||||
Public Property TextMaxLengths As New Dictionary(Of String, Integer) From {
|
||||
{"name", 28},
|
||||
{"position", 32},
|
||||
{"city", 24},
|
||||
{"date", 28}
|
||||
}
|
||||
```
|
||||
|
||||
Default values are starting points and should be tuned against real customer data.
|
||||
|
||||
### 2. `PDFBurner.vb` — Add truncation helper and apply in `AddFormFieldValue`
|
||||
|
||||
#### 2a. Add a private helper function
|
||||
|
||||
```vbnet
|
||||
Private Function TruncateText(value As String, fieldName As String) As String
|
||||
If String.IsNullOrEmpty(value) Then Return value
|
||||
|
||||
Dim maxLen As Integer = 0
|
||||
If _pdfBurnerParams.TextMaxLengths IsNot Nothing AndAlso
|
||||
_pdfBurnerParams.TextMaxLengths.TryGetValue(fieldName, maxLen) AndAlso
|
||||
maxLen > 0 AndAlso value.Length > maxLen Then
|
||||
Return value.Substring(0, maxLen - 3) & "..."
|
||||
End If
|
||||
|
||||
Return value
|
||||
End Function
|
||||
```
|
||||
|
||||
#### 2b. Apply truncation in the `egName`-aware overload
|
||||
|
||||
In `AddFormFieldValue(pAnnotation As Annotation, formFieldValue As FormFieldValue)`:
|
||||
|
||||
```vbnet
|
||||
' Before passing value to AddTextAnnot, truncate:
|
||||
Dim displayValue As String = TruncateText(formFieldValue.value, pAnnotation.egName)
|
||||
|
||||
Dim ant = Manager.AddTextAnnot(oX, oY, oWidth, oHeight, displayValue)
|
||||
```
|
||||
|
||||
#### 2c. Apply truncation in the element-based overload (optional)
|
||||
|
||||
In `AddFormFieldValue(x, y, width, height, page, value)` — if a field name is available in calling context, pass it through; otherwise this path does not use `egName` and truncation can be skipped or applied with a generic fallback key.
|
||||
|
||||
---
|
||||
|
||||
## Affected Files
|
||||
|
||||
| File | Change |
|
||||
|------|--------|
|
||||
| `EnvelopeGenerator.CommonServices/Jobs/FinalizeDocument/PDFBurnerParams.vb` | Add `TextMaxLengths As Dictionary(Of String, Integer)` |
|
||||
| `EnvelopeGenerator.CommonServices/Jobs/FinalizeDocument/PDFBurner.vb` | Add `TruncateText()` helper; apply in `AddFormFieldValue` overloads |
|
||||
|
||||
---
|
||||
|
||||
## Notes
|
||||
|
||||
- Field names (`"name"`, `"position"`, `"city"`, `"date"`) must match the keys used in `EGName.Index` dictionary (`PDFBurner.vb:443–450`).
|
||||
- The certificate hash field (e.g. `C6B85E96A520C60...`) is already truncated upstream — verify its `egName` mapping if it needs its own limit.
|
||||
- The `YOffset` / `TopMargin` values in `PDFBurnerParams` may also need adjustment to increase vertical spacing between rows. That is a separate (simpler) change.
|
||||
- This issue is **not** related to the Blazor WebUI rendering — it is purely in the GdPicture14 PDF burning pipeline.
|
||||
Reference in New Issue
Block a user