# 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 --- ## ✅ 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; /// CSS height, e.g. "28px" or "2rem". Change per deployment in appsettings. public string Height { get; set; } = "28px"; public Dictionary 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( 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 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 signFLOW ``` #### `EnvelopeSenderPage.razor` — signFLOW Logo im Action Bar ```razor
signFLOW
Umschlag-Übersicht
signFLOW Portal - Übersicht
``` --- ## ✅ 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
@if (!string.IsNullOrEmpty(_companyLogo?.Src)) { Kunden-Logo } else {
Kunden-Logo
}
``` #### `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 ``` ### Grid Persistence Hint Info-Banner über dem Grid: ```razor
Die Größe und Position der Spalten wird automatisch für Sie gespeichert.
``` ```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) └── └── └── (SSR-only) ``` #### `Server/Components/Shared/EnvelopePdfPreviewComponent.razor` — NEU ```razor @rendermode InteractiveServer @inject IDocumentService DocumentService @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. ```csharp 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): ```razor Nur ausgewählte ``` --- ## 🔲 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 | 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*