Files
EnvelopeGenerator/EnvelopeGenerator.Server/SF-74-PLAN.md
TekH ed1468194d Add export functionality to /sender Dashboard (Phase 6e)
Finalized the implementation of the export feature for the
`EnvelopeSenderPage.razor` in the Blazor WASM Client. The feature
supports exporting grid data in XLSX, CSV, and PDF formats, with
an option to export only selected rows.

- Updated `SF-74-PLAN.6e.md` to reflect the completed Phase 6e.
- Integrated a compact UI for export controls in the toolbar:
  - Dropdown for format selection (XLSX, CSV, PDF).
  - Checkbox for "Only selected rows" option.
  - Single `Exportieren` button to trigger the export.
- Implemented `ExportSelectedFormatAsync` to handle export logic:
  - Supported formats: XLSX, CSV, PDF.
  - Added options for `ExportSelectedRowsOnly` and grouping.
- Replaced placeholder `ExportToExcelAsync` with finalized logic.
- Documented known limitations (e.g., template columns not exported).
- Updated project status table to mark Phase 6e as complete.

This commit ensures a streamlined and user-friendly export UX,
consistent with the existing design, while addressing known
DevExpress limitations.
2026-09-24 18:38:11 +02:00

15 KiB
Raw Blame History

SF-74 — signFLOW: Theme, Dark Mode, Grid Persistence, Logo & Dashboard

Ticket: SF-74 | Bearbeiter: Hakan Tek | Status: In Bearbeitung
Letzte Aktualisierung: 2026-09-24
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–6e)

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 ✅
6d Dokument anzeigen (PDF Preview Popup mit DxPdfViewer) ✅
6e Export (XLSX/CSV/PDF + Selected Rows Only) ✅

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


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

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)

"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

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

// 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

.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

<!-- 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

<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)

<!-- 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

.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)

.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)

<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:

<div class="sf-grid-persistence-hint">
    <svg><!-- info icon --></svg>
    Die Größe und Position der Spalten wird automatisch für Sie gespeichert.
</div>
.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

@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 (XLSX/CSV/PDF)

Technischer Ansatz

DxGrid-Export direkt im Razor-Code, keine Backend-Implementierung nötig.
Download geht direkt an den Browser.

async Task ExportSelectedFormatAsync()
{
    var grid = _activeTab == "active" ? _gridActive : _gridCompleted;
    if (grid is null) return;

    var baseName = $"Umschlaege_{DateTime.Now:yyyy-MM-dd}";
    switch (_selectedExportFormat)
    {
        case "csv":
            await grid.ExportToCsvAsync(baseName, new GridCsvExportOptions {
                ExportSelectedRowsOnly = _exportSelectedRowsOnly
            });
            break;
        case "pdf":
            await grid.ExportToPdfAsync(baseName, new GridPdfExportOptions {
                ExportSelectedRowsOnly = _exportSelectedRowsOnly,
                SelectedRowsExportMode = GridSelectedRowsExportMode.KeepGrouping
            });
            break;
        default:
            await grid.ExportToXlsxAsync(baseName, new GridXlExportOptions {
                ExportSelectedRowsOnly = _exportSelectedRowsOnly,
                SelectedRowsExportMode = GridSelectedRowsExportMode.KeepGrouping
            });
            break;
    }
}

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.

UI in Toolbar (kompakt):

<select @bind="_selectedExportFormat">
  <option value="xlsx">XLSX</option>
  <option value="csv">CSV</option>
  <option value="pdf">PDF</option>
</select>
<input type="checkbox" @bind="_exportSelectedRowsOnly" /> Nur ausgewählte
<button @onclick="ExportSelectedFormatAsync">Exportieren</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

.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 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