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.
15 KiB
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):
- signFLOW-Logo + Kunden-Logo-Bereich auf der Startseite
- DevExpress Skin-Auswahl — mindestens 3 Themes, vom User wählbar + gespeichert
- 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
✅ 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
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+Heightinappsettings.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.pngdigital_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:
EnvelopeTypeTitlewird aus dem Navigation PropertyEnvelopeTypegemappt. Mapping-Konfiguration inApplication/...MappingProfile.csverifizieren.
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 SpaltenStatus,EnvelopeReceiversnutzen Templates → Wert im Export ist leer.
Future Task: Für vollständigen Export mit formatierten ZellenCustomizeCell-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ückgerufengesetzt 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