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

443 lines
15 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 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;
/// <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 (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
<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`
```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*