diff --git a/EnvelopeGenerator.ReceiverUI/EnvelopeGenerator.ReceiverUI.Client/Components/Envelope/AccessCodeForm.razor b/EnvelopeGenerator.ReceiverUI/EnvelopeGenerator.ReceiverUI.Client/Components/Envelope/AccessCodeForm.razor
index 1113b905..a966327f 100644
--- a/EnvelopeGenerator.ReceiverUI/EnvelopeGenerator.ReceiverUI.Client/Components/Envelope/AccessCodeForm.razor
+++ b/EnvelopeGenerator.ReceiverUI/EnvelopeGenerator.ReceiverUI.Client/Components/Envelope/AccessCodeForm.razor
@@ -1,56 +1,168 @@
-@* DUMB COMPONENT: Kennt keine Services, nur Parameter und Events *@
+@* AccessCodeForm: Zeigt das Zugangscode-Eingabeformular.
+ Entspricht dem AccessCode-Teil von EnvelopeLocked.cshtml im Web-Projekt.
+
+ "Dumme" Komponente: Keine Services, keine API-Calls.
+ - Bekommt Daten über [Parameter]
+ - Gibt Eingaben über EventCallback an die Eltern-Page zurück
+ - Die Page entscheidet, was mit dem Code passiert *@
-
-
Zugangscode eingeben
-
Ein Zugangscode wurde an Ihre E-Mail-Adresse gesendet.
+ Ein Zugangscode wurde an Ihre E-Mail-Adresse gesendet.
+ Bitte geben Sie diesen unten ein.
+
+
-
@code {
- // Parameter von der Eltern-Page
- [Parameter] public required string EnvelopeKey { get; set; }
- [Parameter] public string? ErrorMessage { get; set; }
+ // ── Parameter von der Eltern-Page ──
- // EventCallback: Informiert die Page, dass ein Code eingegeben wurde
- [Parameter] public EventCallback OnSubmit { get; set; }
+ /// Der Envelope-Key aus der URL
+ [Parameter, EditorRequired]
+ public string EnvelopeKey { get; set; } = string.Empty;
+
+ /// Fehlermeldung (z.B. "Falscher Zugangscode")
+ [Parameter]
+ public string? ErrorMessage { get; set; }
+
+ /// E-Mail des Absenders — für den Hilfe-Bereich unten
+ [Parameter]
+ public string? SenderEmail { get; set; }
+
+ /// Titel des Umschlags — für den Hilfe-Bereich unten
+ [Parameter]
+ public string? Title { get; set; }
+
+ /// Ob TFA für diesen Umschlag aktiviert ist — zeigt den SMS-Switch
+ [Parameter]
+ public bool TfaEnabled { get; set; }
+
+ /// Ob der Empfänger eine Telefonnummer hat — sonst ist SMS-Switch disabled
+ [Parameter]
+ public bool HasPhoneNumber { get; set; }
+
+ // ── Event: Gibt Code + SMS-Präferenz an die Page zurück ──
+
+ ///
+ /// Callback wenn der Benutzer das Formular abschickt.
+ /// Gibt ein Tuple zurück: (Code, PreferSms)
+ /// Die Page entscheidet dann, was damit passiert (API-Call etc.)
+ ///
+ /// WARUM ein Tuple statt nur string?
+ /// Weil die Page auch wissen muss, ob der Benutzer SMS bevorzugt.
+ /// Im Web-Projekt wird das als separates Form-Feld "userSelectSMS" gesendet.
+ ///
+ [Parameter]
+ public EventCallback<(string Code, bool PreferSms)> OnSubmit { get; set; }
+
+ // ── Interner State ──
private AccessCodeModel _model = new();
private bool _isSubmitting;
+ private bool _preferSms;
+
+ private void ToggleSms(ChangeEventArgs e)
+ {
+ _preferSms = (bool)(e.Value ?? false);
+ }
private async Task Submit()
{
_isSubmitting = true;
- await OnSubmit.InvokeAsync(_model.Code);
+ await OnSubmit.InvokeAsync((_model.Code, _preferSms));
_isSubmitting = false;
}
+ // ── Validierungs-Model ──
+
private class AccessCodeModel
{
[System.ComponentModel.DataAnnotations.Required(ErrorMessage = "Bitte Zugangscode eingeben")]
diff --git a/EnvelopeGenerator.ReceiverUI/EnvelopeGenerator.ReceiverUI.Client/Components/Envelope/ActionPanel.razor b/EnvelopeGenerator.ReceiverUI/EnvelopeGenerator.ReceiverUI.Client/Components/Envelope/ActionPanel.razor
new file mode 100644
index 00000000..eb9b5ba8
--- /dev/null
+++ b/EnvelopeGenerator.ReceiverUI/EnvelopeGenerator.ReceiverUI.Client/Components/Envelope/ActionPanel.razor
@@ -0,0 +1,126 @@
+@* ActionPanel: Fixierte Button-Leiste am unteren Bildschirmrand.
+ Entspricht dem
in ShowEnvelope.cshtml.
+
+ Wird angezeigt wenn der Empfänger das Dokument sieht (Status = ShowDocument).
+ Bei ReadOnly-Dokumenten wird nichts angezeigt.
+
+ Die Buttons lösen EventCallbacks aus. Die Eltern-Komponente (EnvelopePage)
+ entscheidet, was passiert (API-Call etc.).
+
+ Der ConfirmDialog wird über eine Referenz aufgerufen:
+ var confirmed = await _confirmDialog.ShowAsync("Titel", "Text");
+
+ WARUM EventCallbacks statt direkte API-Calls?
+ - Die Komponente bleibt "dumm" (keine Services, kein API-Wissen)
+ - Die Eltern-Page kann den gleichen Button-Click anders behandeln
+ - Einfacher zu testen *@
+
+@if (!ReadOnly)
+{
+ @* position-fixed: Bleibt am unteren Rand auch beim Scrollen.
+ Gleiche Positionierung wie im Web-Projekt. *@
+
+
+ @* Zurücksetzen-Button: Setzt alle Signaturen zurück.
+ Im Web-Projekt ist das der graue Button mit dem
+ Pfeil-gegen-den-Uhrzeigersinn-Icon. *@
+
+
+
+
+ @* Ablehnen-Button: Öffnet ConfirmDialog, dann EventCallback.
+ Im Web-Projekt ist das der rote Button mit dem X-Icon. *@
+
+
+ Ablehnen
+
+
+ @* Unterschreiben-Button: Öffnet ConfirmDialog, dann EventCallback.
+ Im Web-Projekt ist das der grüne Button mit dem Briefumschlag-Icon. *@
+
+
+ Unterschreiben
+
+
+}
+
+@* ConfirmDialog: Wird nur gerendert wenn nötig (wenn ShowAsync aufgerufen wird).
+ Die Referenz (_confirmDialog) erlaubt uns, ShowAsync von den Button-Handlern aufzurufen. *@
+
+
+@code {
+ // ── Parameter ──
+
+ /// Bei ReadOnly wird das gesamte Panel ausgeblendet
+ [Parameter]
+ public bool ReadOnly { get; set; }
+
+ /// Wird ausgelöst wenn der Benutzer "Unterschreiben" bestätigt
+ [Parameter]
+ public EventCallback OnSign { get; set; }
+
+ /// Wird ausgelöst wenn der Benutzer "Ablehnen" bestätigt
+ [Parameter]
+ public EventCallback OnReject { get; set; }
+
+ /// Wird ausgelöst wenn der Benutzer "Zurücksetzen" klickt
+ [Parameter]
+ public EventCallback OnRefresh { get; set; }
+
+ // ── Referenz auf den ConfirmDialog ──
+
+ ///
+ /// Referenz auf die ConfirmDialog-Komponente.
+ ///
+ /// WAS IST @ref?
+ /// Mit @ref speichert Blazor eine Referenz auf eine Kind-Komponente.
+ /// Dann kann man Methoden darauf aufrufen: _confirmDialog.ShowAsync(...)
+ /// Das ist wie document.getElementById() in JavaScript, nur typsicher.
+ ///
+ private ConfirmDialog _confirmDialog = default!;
+
+ // ── Button-Handler ──
+
+ ///
+ /// Unterschreiben: Erst bestätigen, dann Event auslösen.
+ /// Der ConfirmDialog zeigt "Möchten Sie das Dokument unterschreiben?"
+ /// Nur wenn der Benutzer "Ja" klickt, wird OnSign ausgelöst.
+ ///
+ private async Task HandleSign()
+ {
+ var confirmed = await _confirmDialog.ShowAsync(
+ "Unterschreiben",
+ "Möchten Sie das Dokument wirklich unterschreiben? Diese Aktion kann nicht rückgängig gemacht werden.",
+ confirmText: "Unterschreiben",
+ confirmColor: "success");
+
+ if (confirmed)
+ await OnSign.InvokeAsync();
+ }
+
+ ///
+ /// Ablehnen: Erst bestätigen, dann Event auslösen.
+ /// Roter Button im ConfirmDialog, weil Ablehnen destruktiv ist.
+ ///
+ private async Task HandleReject()
+ {
+ var confirmed = await _confirmDialog.ShowAsync(
+ "Ablehnen",
+ "Möchten Sie das Dokument wirklich ablehnen? Diese Aktion kann nicht rückgängig gemacht werden.",
+ confirmText: "Ablehnen",
+ confirmColor: "danger");
+
+ if (confirmed)
+ await OnReject.InvokeAsync();
+ }
+
+ ///
+ /// Zurücksetzen: Kein ConfirmDialog nötig, weil es nicht destruktiv ist.
+ /// Setzt nur die Signatur-Positionen zurück, der Empfänger kann danach
+ /// erneut signieren.
+ ///
+ private async Task HandleRefresh()
+ {
+ await OnRefresh.InvokeAsync();
+ }
+}
\ No newline at end of file
diff --git a/EnvelopeGenerator.ReceiverUI/EnvelopeGenerator.ReceiverUI.Client/Components/Envelope/EnvelopeInfoCard.razor b/EnvelopeGenerator.ReceiverUI/EnvelopeGenerator.ReceiverUI.Client/Components/Envelope/EnvelopeInfoCard.razor
new file mode 100644
index 00000000..9a35d9cc
--- /dev/null
+++ b/EnvelopeGenerator.ReceiverUI/EnvelopeGenerator.ReceiverUI.Client/Components/Envelope/EnvelopeInfoCard.razor
@@ -0,0 +1,121 @@
+@* EnvelopeInfoCard: Zeigt die Umschlag-Infos oberhalb des PDF-Viewers.
+ Entspricht dem Card-Bereich in ShowEnvelope.cshtml im Web-Projekt.
+
+ Wird angezeigt wenn der Empfänger erfolgreich authentifiziert ist
+ und das Dokument sehen darf.
+
+ "Dumme" Komponente: Keine Services, nur Parameter → Anzeige. *@
+
+
+ @* ── Card Header: Logo + Fortschrittsbalken ──
+ Im Web-Projekt gibt es hier das signFLOW-Logo und darunter
+ "2/3 Signatures". Wir zeigen den Fortschritt nur wenn es
+ KEIN ReadOnly-Dokument ist (ReadOnly = nur lesen, nicht signieren). *@
+
+
+ @* Nachricht des Absenders — im Web-Projekt wird das mit Marked.js
+ als Markdown gerendert. Hier erstmal als Plain Text.
+ Markdown-Rendering kommt in Phase 6 (Feinschliff). *@
+ @if (!string.IsNullOrEmpty(Message))
+ {
+
@Message
+ }
+
+ @* Sender-Info: Wer hat es gesendet, wann?
+ Im Web-Projekt steht hier:
+ "Gesendet am 15.03.2026 von Max Mustermann (max@firma.de)" *@
+
+
+ @if (!string.IsNullOrEmpty(SenderName) && !string.IsNullOrEmpty(SenderEmail))
+ {
+
+ Gesendet
+ @if (SentDate is not null)
+ {
+ am @SentDate.Value.ToString("dd.MM.yyyy")
+ }
+ von @SenderName
+ (@SenderEmail)
+
+ }
+
+
+
+
+
+@code {
+ // ── Parameter ──
+
+ /// Titel des Umschlags (z.B. "Vertragsdokument")
+ [Parameter, EditorRequired]
+ public string Title { get; set; } = string.Empty;
+
+ /// Nachricht des Absenders
+ [Parameter]
+ public string? Message { get; set; }
+
+ /// Name des Absenders (z.B. "Max Mustermann")
+ [Parameter]
+ public string? SenderName { get; set; }
+
+ /// E-Mail des Absenders
+ [Parameter]
+ public string? SenderEmail { get; set; }
+
+ /// Datum an dem der Umschlag gesendet wurde
+ [Parameter]
+ public DateTime? SentDate { get; set; }
+
+ /// Ob das Dokument nur zum Lesen ist (kein Signieren)
+ [Parameter]
+ public bool ReadOnly { get; set; }
+
+ /// Anzahl bereits geleisteter Unterschriften
+ [Parameter]
+ public int SignaturesDone { get; set; }
+
+ /// Gesamtanzahl benötigter Unterschriften
+ [Parameter]
+ public int SignatureTotal { get; set; }
+
+ // ── Berechnete Werte ──
+
+ ///
+ /// Fortschritt in Prozent für den Balken.
+ /// Vermeidet Division durch Null.
+ ///
+ private int ProgressPercent =>
+ SignatureTotal > 0
+ ? (int)((double)SignaturesDone / SignatureTotal * 100)
+ : 0;
+}
\ No newline at end of file
diff --git a/EnvelopeGenerator.ReceiverUI/EnvelopeGenerator.ReceiverUI.Client/Components/Envelope/TfaForm.razor b/EnvelopeGenerator.ReceiverUI/EnvelopeGenerator.ReceiverUI.Client/Components/Envelope/TfaForm.razor
new file mode 100644
index 00000000..67bb0c55
--- /dev/null
+++ b/EnvelopeGenerator.ReceiverUI/EnvelopeGenerator.ReceiverUI.Client/Components/Envelope/TfaForm.razor
@@ -0,0 +1,202 @@
+@* TfaForm: Zwei-Faktor-Authentifizierung — SMS oder Authenticator.
+ Entspricht dem TFA-Teil von EnvelopeLocked.cshtml im Web-Projekt.
+
+ Wird angezeigt NACH dem AccessCode-Schritt, wenn der Umschlag TFA erfordert.
+
+ Zwei Varianten:
+ - TfaType="sms" → SMS wurde gesendet, Countdown-Timer zeigt verbleibende Zeit
+ - TfaType="authenticator" → Benutzer gibt Code aus seiner Authenticator-App ein
+
+ "Dumme" Komponente: Keine Services, keine API-Calls. *@
+
+@implements IDisposable
+
+
+ Ein Bestätigungscode wurde per SMS an Ihre Telefonnummer gesendet.
+
+ @* SMS-Countdown-Timer: Zeigt wie lange der Code noch gültig ist.
+ Im Web-Projekt ist das JavaScript (setInterval).
+ Hier machen wir es mit einem C#-Timer — kein JS nötig. *@
+ @if (_remainingTime is not null)
+ {
+
+ @if (_remainingTime > TimeSpan.Zero)
+ {
+ Code gültig für: @_remainingTime.Value.ToString("mm\\:ss")
+ }
+ else
+ {
+ Code abgelaufen. Bitte fordern Sie einen neuen Code an.
+ }
+
+ }
+ }
+ else
+ {
+
+ Öffnen Sie Ihre Authenticator-App und geben Sie den angezeigten Code ein.
+
+
+@code {
+ // ── Parameter von der Eltern-Page ──
+
+ /// Der Envelope-Key aus der URL
+ [Parameter, EditorRequired]
+ public string EnvelopeKey { get; set; } = string.Empty;
+
+ ///
+ /// "sms" oder "authenticator" — bestimmt welche Variante angezeigt wird.
+ /// Kommt aus der API-Antwort nach dem AccessCode-Schritt.
+ ///
+ [Parameter, EditorRequired]
+ public string TfaType { get; set; } = "authenticator";
+
+ ///
+ /// Ablaufzeit des SMS-Codes. Nur bei TfaType="sms" relevant.
+ /// Der Timer zählt von jetzt bis zu diesem Zeitpunkt runter.
+ ///
+ [Parameter]
+ public DateTime? TfaExpiration { get; set; }
+
+ /// Ob der Empfänger eine Telefonnummer hat
+ [Parameter]
+ public bool HasPhoneNumber { get; set; }
+
+ /// Fehlermeldung (z.B. "Falscher Code")
+ [Parameter]
+ public string? ErrorMessage { get; set; }
+
+ ///
+ /// Callback: Gibt (Code, Type) an die Page zurück.
+ /// Die Page sendet das dann an die API.
+ ///
+ [Parameter]
+ public EventCallback<(string Code, string Type)> OnSubmit { get; set; }
+
+ // ── Interner State ──
+
+ private TfaCodeModel _model = new();
+ private bool _isSubmitting;
+ private TimeSpan? _remainingTime;
+ private System.Threading.Timer? _timer;
+
+ // ── Lifecycle ──
+
+ ///
+ /// Wird aufgerufen wenn die Komponente initialisiert wird.
+ /// Startet den SMS-Countdown-Timer falls nötig.
+ ///
+ /// OnInitialized (nicht Async) reicht hier, weil wir keinen
+ /// await brauchen — der Timer läuft über einen Callback.
+ ///
+ protected override void OnInitialized()
+ {
+ if (TfaType == "sms" && TfaExpiration is not null)
+ {
+ // Restzeit berechnen
+ _remainingTime = TfaExpiration.Value - DateTime.Now;
+
+ // Timer: Jede Sekunde _remainingTime aktualisieren.
+ // InvokeAsync + StateHasChanged sagt Blazor: "Zeichne die UI neu".
+ // Das ist das Blazor-Äquivalent zu setInterval + DOM-Update in JavaScript.
+ _timer = new System.Threading.Timer(_ =>
+ {
+ _remainingTime = TfaExpiration.Value - DateTime.Now;
+
+ if (_remainingTime <= TimeSpan.Zero)
+ {
+ _remainingTime = TimeSpan.Zero;
+ _timer?.Dispose();
+ }
+
+ // InvokeAsync ist nötig, weil der Timer auf einem anderen Thread läuft.
+ // Blazor erlaubt UI-Updates nur auf dem UI-Thread.
+ InvokeAsync(StateHasChanged);
+ },
+ state: null,
+ dueTime: TimeSpan.Zero, // Sofort starten
+ period: TimeSpan.FromSeconds(1) // Jede Sekunde
+ );
+ }
+ }
+
+ private async Task Submit()
+ {
+ _isSubmitting = true;
+ await OnSubmit.InvokeAsync((_model.Code, TfaType));
+ _isSubmitting = false;
+ }
+
+ ///
+ /// Aufräumen: Timer stoppen wenn die Komponente entfernt wird.
+ /// Ohne Dispose würde der Timer weiterlaufen und Fehler verursachen,
+ /// weil er versucht eine nicht mehr existierende UI zu aktualisieren.
+ ///
+ public void Dispose()
+ {
+ _timer?.Dispose();
+ }
+
+ // ── Validierungs-Model ──
+
+ private class TfaCodeModel
+ {
+ [System.ComponentModel.DataAnnotations.Required(ErrorMessage = "Bitte Code eingeben")]
+ [System.ComponentModel.DataAnnotations.StringLength(6, MinimumLength = 6, ErrorMessage = "Der Code muss 6 Zeichen lang sein")]
+ public string Code { get; set; } = string.Empty;
+ }
+}
\ No newline at end of file
diff --git a/EnvelopeGenerator.ReceiverUI/EnvelopeGenerator.ReceiverUI.Client/Components/Shared/ConfirmDialog.razor b/EnvelopeGenerator.ReceiverUI/EnvelopeGenerator.ReceiverUI.Client/Components/Shared/ConfirmDialog.razor
new file mode 100644
index 00000000..d1892b1c
--- /dev/null
+++ b/EnvelopeGenerator.ReceiverUI/EnvelopeGenerator.ReceiverUI.Client/Components/Shared/ConfirmDialog.razor
@@ -0,0 +1,133 @@
+@* ConfirmDialog: Ersetzt SweetAlert2 aus dem Web-Projekt.
+
+ Zeigt einen modalen Bestätigungsdialog mit Titel, Text und Ja/Nein-Buttons.
+ Wird NICHT über Parameter gesteuert, sondern über eine Methode:
+
+ var confirmed = await _dialog.ShowAsync("Titel", "Text");
+
+ WARUM eine Methode statt Parameter?
+ - Ein Dialog ist ein "einmaliges Ereignis", kein dauerhafter Zustand
+ - Die aufrufende Komponente will auf das Ergebnis WARTEN (await)
+ - Mit Parametern müsste man den State manuell hin- und herschalten
+
+ WARUM kein SweetAlert2?
+ - SweetAlert2 ist eine JavaScript-Bibliothek
+ - In Blazor können wir das nativ in C# lösen, ohne JS-Interop
+ - Weniger Abhängigkeiten = weniger Wartung = weniger Fehlerquellen *@
+
+@if (_isVisible)
+{
+ @* Hintergrund-Overlay: Dunkler Hintergrund hinter dem Dialog.
+ Im Web-Projekt macht SweetAlert2 das automatisch.
+ Hier bauen wir es selbst mit CSS. *@
+
+
+ @* Modal-Dialog: Bootstrap 5 Modal-Markup.
+ Normalerweise braucht Bootstrap-Modal JavaScript (bootstrap.js)
+ um zu öffnen/schließen. Wir steuern die Sichtbarkeit stattdessen
+ über die _isVisible-Variable — Blazor macht das DOM-Update. *@
+
+
+
+
+ @* Header: Titel + Schließen-Button *@
+
+
@_title
+
+
+
+ @* Body: Beschreibungstext *@
+
+
@_message
+
+
+ @* Footer: Abbrechen + Bestätigen *@
+
+
+
+
+
+}
+
+@code {
+ // ── Interner State (NICHT Parameter — wird über ShowAsync gesetzt) ──
+
+ private bool _isVisible;
+ private string _title = string.Empty;
+ private string _message = string.Empty;
+ private string _confirmText = "Ja";
+ private string _cancelText = "Abbrechen";
+ private string _confirmColor = "primary";
+
+ ///
+ /// TaskCompletionSource: Das Herzstück dieser Komponente.
+ ///
+ /// WAS IST DAS?
+ /// Ein TaskCompletionSource erstellt einen Task, der erst dann
+ /// "fertig" wird, wenn jemand SetResult() aufruft.
+ ///
+ /// WIE FUNKTIONIERT ES?
+ /// 1. ShowAsync() erstellt ein neues TaskCompletionSource
+ /// 2. ShowAsync() gibt dessen Task zurück → der Aufrufer wartet (await)
+ /// 3. Der Benutzer klickt "Ja" → Confirm() ruft SetResult(true) auf
+ /// 4. Der await in der aufrufenden Komponente kommt zurück mit true
+ ///
+ /// Das ist wie ein "Promise" in JavaScript, nur in C#.
+ ///
+ private TaskCompletionSource? _tcs;
+
+ ///
+ /// Zeigt den Dialog und wartet auf die Benutzer-Entscheidung.
+ ///
+ /// Beispiel-Aufruf:
+ /// var confirmed = await _dialog.ShowAsync(
+ /// "Unterschreiben",
+ /// "Möchten Sie das Dokument wirklich unterschreiben?");
+ ///
+ /// Dialog-Überschrift
+ /// Beschreibungstext
+ /// Text auf dem Bestätigen-Button (Standard: "Ja")
+ /// Text auf dem Abbrechen-Button (Standard: "Abbrechen")
+ /// Bootstrap-Farbe des Bestätigen-Buttons (Standard: "primary")
+ /// true wenn bestätigt, false wenn abgebrochen
+ public Task ShowAsync(
+ string title,
+ string message,
+ string confirmText = "Ja",
+ string cancelText = "Abbrechen",
+ string confirmColor = "primary")
+ {
+ _title = title;
+ _message = message;
+ _confirmText = confirmText;
+ _cancelText = cancelText;
+ _confirmColor = confirmColor;
+
+ _tcs = new TaskCompletionSource();
+ _isVisible = true;
+ StateHasChanged();
+
+ return _tcs.Task;
+ }
+
+ private void Confirm()
+ {
+ _isVisible = false;
+ _tcs?.TrySetResult(true);
+ StateHasChanged();
+ }
+
+ private void Cancel()
+ {
+ _isVisible = false;
+ _tcs?.TrySetResult(false);
+ StateHasChanged();
+ }
+}
\ No newline at end of file
diff --git a/EnvelopeGenerator.ReceiverUI/EnvelopeGenerator.ReceiverUI.Client/Components/Shared/StatusPage.razor b/EnvelopeGenerator.ReceiverUI/EnvelopeGenerator.ReceiverUI.Client/Components/Shared/StatusPage.razor
new file mode 100644
index 00000000..091167e7
--- /dev/null
+++ b/EnvelopeGenerator.ReceiverUI/EnvelopeGenerator.ReceiverUI.Client/Components/Shared/StatusPage.razor
@@ -0,0 +1,79 @@
+@* StatusPage: Wiederverwendbare Status-Seite für alle Endzustände.
+ Ersetzt EnvelopeSigned.cshtml, EnvelopeRejected.cshtml, Error-Views und Expired.
+ "Dumme" Komponente: Keine Services, keine API-Calls, nur Parameter → Anzeige. *@
+
+
+
+ @switch (Type)
+ {
+ case "signed":
+
+
+
+
Dokument erfolgreich unterschrieben
+
+ Sie erhalten eine Bestätigung per E-Mail, sobald alle Empfänger unterschrieben haben.
+
+ break;
+
+ case "rejected":
+
+
+
+
Dokument wurde abgelehnt
+
+ @if (!string.IsNullOrEmpty(Title) && !string.IsNullOrEmpty(SenderEmail))
+ {
+
+ Das Dokument «@Title» wurde abgelehnt.
+ Bei Fragen wenden Sie sich an
+ @SenderEmail.
+
+ }
+ else
+ {
+ Dieses Dokument wurde von einem Empfänger abgelehnt.
+ }
+
+ break;
+
+ case "not_found":
+
+
+
+
Dokument nicht gefunden
+
+ Dieses Dokument existiert nicht oder ist nicht mehr verfügbar.
+ Wenn Sie diese URL per E-Mail erhalten haben, wenden Sie sich bitte an das IT-Team.
+
+ break;
+
+ case "expired":
+
+
+
+
Link abgelaufen
+
+ Der Zugang zu diesem Dokument ist abgelaufen.
+
+ break;
+ }
+
+
+
+@code {
+ ///
+ /// Bestimmt welche Status-Variante angezeigt wird.
+ /// Erlaubte Werte: "signed", "rejected", "not_found", "expired"
+ ///
+ [Parameter, EditorRequired]
+ public string Type { get; set; } = string.Empty;
+
+ /// E-Mail des Absenders — nur bei "rejected" relevant.
+ [Parameter]
+ public string? SenderEmail { get; set; }
+
+ /// Titel des Umschlags — nur bei "rejected" relevant.
+ [Parameter]
+ public string? Title { get; set; }
+}
\ No newline at end of file
diff --git a/EnvelopeGenerator.ReceiverUI/EnvelopeGenerator.ReceiverUI.Client/Components/Shared/Toast.razor b/EnvelopeGenerator.ReceiverUI/EnvelopeGenerator.ReceiverUI.Client/Components/Shared/Toast.razor
new file mode 100644
index 00000000..d67475ea
--- /dev/null
+++ b/EnvelopeGenerator.ReceiverUI/EnvelopeGenerator.ReceiverUI.Client/Components/Shared/Toast.razor
@@ -0,0 +1,70 @@
+@* Toast: Zeigt kurze Benachrichtigungen an. Ersetzt AlertifyJS aus dem Web-Projekt.
+
+ Wird einmal im MainLayout platziert. Hört auf den ToastService.
+ Wenn eine Komponente irgendwo _toast.ShowSuccess("Text") aufruft,
+ erscheint hier ein Toast und verschwindet nach 4 Sekunden automatisch.
+
+ Bootstrap Toast-Klassen werden genutzt:
+ - toast-container: Positionierung (oben rechts)
+ - toast: Einzelner Toast
+ - bg-success/bg-danger/etc: Farbe basierend auf Typ *@
+
+@inject ToastService ToastService
+@implements IDisposable
+
+@if (ToastService.Messages.Count > 0)
+{
+ @* toast-container: Bootstrap-Klasse die Toasts oben rechts positioniert.
+ position-fixed: Bleibt an der Stelle auch beim Scrollen.
+ z-index 1080: Über dem Modal-Backdrop (1070) damit Toasts
+ auch während eines ConfirmDialogs sichtbar sind. *@
+
+ @foreach (var message in ToastService.Messages)
+ {
+
+
+
+
+ @message.Text
+
+ ToastService.Dismiss(message)">
+
+
+
+ }
+
+}
+
+@code {
+ protected override void OnInitialized()
+ {
+ // Subscriber: Wenn der ToastService eine Änderung meldet,
+ // zeichnet diese Komponente sich neu (StateHasChanged).
+ // So erscheinen/verschwinden Toasts automatisch.
+ ToastService.OnChange += HandleChange;
+ }
+
+ ///
+ /// InvokeAsync ist nötig weil OnChange von einem
+ /// async void (dem Timer im ToastService) ausgelöst wird.
+ /// Das kann auf einem anderen Thread passieren.
+ /// InvokeAsync wechselt zurück auf den Blazor UI-Thread.
+ ///
+ private async void HandleChange()
+ {
+ await InvokeAsync(StateHasChanged);
+ }
+
+ ///
+ /// Event-Handler abmelden wenn die Komponente entfernt wird.
+ /// Ohne das würde der ToastService eine Referenz auf eine
+ /// nicht mehr existierende Komponente halten → Memory Leak.
+ ///
+ public void Dispose()
+ {
+ ToastService.OnChange -= HandleChange;
+ }
+}
\ No newline at end of file
diff --git a/EnvelopeGenerator.ReceiverUI/EnvelopeGenerator.ReceiverUI.Client/Program.cs b/EnvelopeGenerator.ReceiverUI/EnvelopeGenerator.ReceiverUI.Client/Program.cs
index fc688bf6..ee4d8011 100644
--- a/EnvelopeGenerator.ReceiverUI/EnvelopeGenerator.ReceiverUI.Client/Program.cs
+++ b/EnvelopeGenerator.ReceiverUI/EnvelopeGenerator.ReceiverUI.Client/Program.cs
@@ -24,4 +24,6 @@ builder.Services.AddScoped();
// State: Ein State-Objekt pro Browser-Tab
builder.Services.AddScoped();
+builder.Services.AddScoped();
+
await builder.Build().RunAsync();
\ No newline at end of file
diff --git a/EnvelopeGenerator.ReceiverUI/EnvelopeGenerator.ReceiverUI.Client/Services/ToastService.cs b/EnvelopeGenerator.ReceiverUI/EnvelopeGenerator.ReceiverUI.Client/Services/ToastService.cs
new file mode 100644
index 00000000..2aead5ee
--- /dev/null
+++ b/EnvelopeGenerator.ReceiverUI/EnvelopeGenerator.ReceiverUI.Client/Services/ToastService.cs
@@ -0,0 +1,86 @@
+namespace EnvelopeGenerator.ReceiverUI.Client.Services;
+
+///
+/// Service für Toast-Benachrichtigungen. Ersetzt AlertifyJS aus dem Web-Projekt.
+///
+/// WARUM ein Service und keine Komponente mit Parametern?
+/// - Toasts können von ÜBERALL ausgelöst werden (Pages, Services, andere Komponenten)
+/// - Ein Service ist über Dependency Injection überall verfügbar
+/// - Die Toast-Komponente im Layout hört auf diesen Service und rendert die Nachrichten
+///
+/// PATTERN: Pub/Sub (Publisher/Subscriber)
+/// - Publisher: Jede Komponente die _toast.ShowSuccess("...") aufruft
+/// - Subscriber: Die Toast-Komponente im MainLayout, die auf OnChange hört
+///
+/// Das ist das gleiche Pattern wie beim EnvelopeState.
+///
+public class ToastService
+{
+ ///
+ /// Liste aller aktuell sichtbaren Toasts.
+ /// Mehrere Toasts können gleichzeitig angezeigt werden (gestapelt).
+ ///
+ public List Messages { get; } = [];
+
+ /// Event: Informiert die Toast-Komponente über Änderungen
+ public event Action? OnChange;
+
+ public void ShowSuccess(string text) => Show(text, "success");
+ public void ShowError(string text) => Show(text, "danger");
+ public void ShowInfo(string text) => Show(text, "info");
+ public void ShowWarning(string text) => Show(text, "warning");
+
+ ///
+ /// Fügt einen Toast hinzu und entfernt ihn nach der angegebenen Dauer automatisch.
+ ///
+ /// WARUM async void?
+ /// Normalerweise vermeidet man async void. Hier ist es ok, weil:
+ /// - Es ist ein Fire-and-Forget-Timer (wir warten nicht auf das Ergebnis)
+ /// - Fehler im Delay können die App nicht zum Absturz bringen
+ /// - Das ist ein gängiges Pattern für Auto-Dismiss-Logik
+ ///
+ private async void Show(string text, string type, int durationMs = 4000)
+ {
+ var message = new ToastMessage(text, type);
+ Messages.Add(message);
+ OnChange?.Invoke();
+
+ // Nach Ablauf der Dauer automatisch entfernen
+ await Task.Delay(durationMs);
+ Messages.Remove(message);
+ OnChange?.Invoke();
+ }
+
+ /// Entfernt einen Toast sofort (z.B. wenn der Benutzer auf X klickt)
+ public void Dismiss(ToastMessage message)
+ {
+ Messages.Remove(message);
+ OnChange?.Invoke();
+ }
+}
+
+///
+/// Ein einzelner Toast-Eintrag.
+///
+/// WARUM ein record statt class?
+/// - Records haben automatisch Equals/GetHashCode basierend auf allen Properties
+/// - Wir brauchen das für Messages.Remove() — es vergleicht über Referenz-Gleichheit
+/// - Die Id (Guid) macht jeden Toast einzigartig, auch bei gleichem Text
+///
+public record ToastMessage(string Text, string Type)
+{
+ /// Eindeutige Id — damit zwei Toasts mit gleichem Text unterscheidbar sind
+ public Guid Id { get; } = Guid.NewGuid();
+
+ ///
+ /// Gibt die Bootstrap-Icon-Klasse basierend auf dem Typ zurück.
+ /// success → check-circle, danger → x-circle, etc.
+ ///
+ public string IconClass => Type switch
+ {
+ "success" => "bi-check-circle-fill",
+ "danger" => "bi-x-circle-fill",
+ "warning" => "bi-exclamation-triangle-fill",
+ _ => "bi-info-circle-fill"
+ };
+}
\ No newline at end of file
diff --git a/EnvelopeGenerator.ReceiverUI/EnvelopeGenerator.ReceiverUI/Components/Layout/MainLayout.razor b/EnvelopeGenerator.ReceiverUI/EnvelopeGenerator.ReceiverUI/Components/Layout/MainLayout.razor
index e28f2bfb..ec279200 100644
--- a/EnvelopeGenerator.ReceiverUI/EnvelopeGenerator.ReceiverUI/Components/Layout/MainLayout.razor
+++ b/EnvelopeGenerator.ReceiverUI/EnvelopeGenerator.ReceiverUI/Components/Layout/MainLayout.razor
@@ -1,12 +1,35 @@
-@inherits LayoutComponentBase
+@* MainLayout: Das Grundgerüst jeder Seite.
+ Entspricht _Layout.cshtml im Web-Projekt.
+
+ Aufbau:
+ - Header: signFLOW-Logo/Titel (oben, sticky)
+ - Main: Der Seiteninhalt (@Body) mit ErrorBoundary
+ - Footer: Copyright + Privacy-Link (unten)
+
+ Sticky Footer Pattern: Der Footer klebt immer am unteren Rand,
+ auch wenn der Inhalt wenig Platz braucht. Das funktioniert über
+ Flexbox in app.css (.app-container mit min-height: 100vh). *@
+
+@inherits LayoutComponentBase
+
+
+ @* ── Header ── *@
-
+
+ @* Im Web-Projekt steht hier ein mit dem signFLOW-Logo.
+ Wir nutzen erstmal Text. Das Logo kommt in Phase 6
+ wenn wir die Bilder aus dem Web-Projekt portieren. *@
signFLOW
+ @* ── Main: Seiteninhalt mit Error-Schutz ──
+ ErrorBoundary fängt unbehandelte Exceptions in Komponenten ab.
+ Ohne ErrorBoundary würde die gesamte App abstürzen.
+ Mit ErrorBoundary zeigen wir stattdessen eine Fehlermeldung
+ und einen "Erneut versuchen"-Button. *@
@@ -14,21 +37,51 @@