fix(files): report success cancellation and errors with safe next actions (U02)

This commit is contained in:
2026-11-02 06:23:59 +01:00
parent a3c1bee09e
commit 5e0ad197f0
15 changed files with 288 additions and 200 deletions
@@ -0,0 +1,40 @@
@inject IFileSaveService FileSaveService
@if (Result != null)
{
<div class="alert @(Result.Status == FileOperationStatus.Error ? "alert-danger" : Result.Status == FileOperationStatus.Success ? "alert-success" : "alert-info")" role="status" aria-live="polite" aria-atomic="true">
<p>@Result.Message</p>
@if (Result.Path != null)
{
<p class="text-break">@Result.Path</p>
<button class="btn btn-outline-secondary" disabled="@busy" @onclick="OpenFolder">Ordner öffnen</button>
}
@if (Result.Details != null)
{
<details><summary>Technische Details</summary><p class="text-break">@Result.Details</p></details>
}
@if (openResult != null)
{
<p role="alert">@openResult.Message</p>
@if (openResult.Details != null) { <details><summary>Technische Details</summary>@openResult.Details</details> }
}
</div>
}
@code {
[Parameter] public FileOperationResult? Result { get; set; }
private FileOperationResult? openResult;
private FileOperationResult? previous;
private bool busy;
protected override void OnParametersSet()
{
if (previous != Result) { openResult = null; previous = Result; }
}
private async Task OpenFolder()
{
if (busy || Result?.Path == null) return;
busy = true;
try { openResult = await FileSaveService.OpenFolderAsync(Result.Path); }
finally { busy = false; }
}
}
@@ -7,6 +7,9 @@
@inject NavigationManager Navigation
<div class="container-fluid">
<OperationNotice Result="fileResult" />
@if (fileBusy) { <p role="status" aria-live="polite">Dateiaktion läuft … Bitte warten.</p> }
@if (account == null)
{
<div class="text-center py-5">
@@ -35,7 +38,7 @@
<button class="btn btn-info btn-nav" @onclick="() => showAddTransfer = true">
<i class="bi bi-arrow-left-right"></i> Umbuchung
</button>
<button class="btn btn-dark btn-nav" @onclick="HandleExport">
<button class="btn btn-dark btn-nav" @onclick="HandleExport" disabled="@fileBusy">
<i class="bi bi-file-earmark-pdf"></i> PDF
</button>
|
@@ -32,6 +32,8 @@ public partial class AccountDetail
private string? confirmRestoreEntryTitle;
private string? savedPdfPath;
private FileOperationResult? fileResult;
private bool fileBusy;
private EntryDto? editingEntry;
private EntryDto? editingTransferEntry;
@@ -178,19 +180,39 @@ public partial class AccountDetail
private async Task HandleExport()
{
if (fileBusy) return;
fileBusy = true;
fileResult = null;
try
{
var pdf = await PdfStatementService.GenerateStatementAsync(AccountId, selectedYear);
var suffix = selectedYear.HasValue ? $"_{selectedYear.Value}" : "_Gesamt";
savedPdfPath = await FileSaveService.SaveFileAsync(pdf, $"{account?.Name}{suffix}.pdf");
fileResult = await FileSaveService.SaveFileAsync(pdf, $"{account?.Name}{suffix}.pdf");
savedPdfPath = fileResult.Status == FileOperationStatus.Success ? fileResult.Path : null;
}
catch (Exception ex) { fileResult = FileOperationResult.Error(ex); }
finally { fileBusy = false; }
}
private async Task HandleOpenSavedPdf()
{
if (fileBusy) return;
fileBusy = true;
fileResult = null;
try
{
if (!string.IsNullOrWhiteSpace(savedPdfPath))
{
await FileSaveService.OpenFileAsync(savedPdfPath);
fileResult = await FileSaveService.OpenFileAsync(savedPdfPath);
}
savedPdfPath = null;
}
catch (Exception ex) { fileResult = FileOperationResult.Error(ex); }
finally { fileBusy = false; }
}
private void CancelOpenSavedPdf()
+6 -9
View File
@@ -8,6 +8,9 @@
@inject NavigationManager NavigationManager
<div class="container-fluid">
<OperationNotice Result="fileResult" />
@if (fileBusy) { <p role="status" aria-live="polite">Dateiaktion läuft … Bitte warten.</p> }
<div class="d-flex justify-content-between align-items-center">
<div class="d-flex align-items-center flex-grow-1" style="min-width: 0;">
@@ -25,14 +28,14 @@
<i class="bi bi-plus-lg"></i> Neues Konto
</button>
|
<button class="btn-nav btn-success" @onclick="HandleBackupAsync">
<button class="btn-nav btn-success" @onclick="HandleBackupAsync" disabled="@fileBusy">
<i class="bi bi-save"></i> Backup
</button>
<button class="btn-nav btn-warning" @onclick="HandleRestoreAsync">
<button class="btn-nav btn-warning" @onclick="HandleRestoreAsync" disabled="@fileBusy">
<i class="bi bi-arrow-counterclockwise"></i> Restore
</button>
|
<button class="btn-nav btn-dark" @onclick="HandleDashboardExportAsync">
<button class="btn-nav btn-dark" @onclick="HandleDashboardExportAsync" disabled="@fileBusy">
<i class="bi bi-file-earmark-pdf"></i> PDF
</button>
|
@@ -54,12 +57,6 @@
</div>
}
@if (!string.IsNullOrWhiteSpace(operationMessage))
{
<div class="alert @operationMessageClass mb-3" role="alert">
@operationMessage
</div>
}
@if (accounts == null)
{
+42 -23
View File
@@ -17,9 +17,9 @@ public partial class Dashboard
private int? selectedYear = DateTime.Now.Year;
private List<int> availableYears = [];
private string clubName = string.Empty;
private string? operationMessage;
private string operationMessageClass = "alert-info";
private string? savedPdfPath;
private FileOperationResult? fileResult;
private bool fileBusy;
private string currentTheme = "light";
#endregion
@@ -100,11 +100,16 @@ public partial class Dashboard
private async Task HandleBackupAsync()
{
var message = await BackupService.CreateBackupAsync();
if (message.Contains("fehlgeschlagen", StringComparison.OrdinalIgnoreCase))
if (fileBusy) return;
fileBusy = true;
fileResult = null;
try
{
SetOperationMessage(message, false);
fileResult = await BackupService.CreateBackupAsync();
}
catch (Exception ex) { fileResult = FileOperationResult.Error(ex); }
finally { fileBusy = false; }
}
private Task HandleRestoreAsync()
@@ -120,25 +125,36 @@ public partial class Dashboard
private async Task HandleConfirmRestore()
{
showRestoreConfirm = false;
var message = await BackupService.RestoreBackupAsync();
var isSuccess = message.StartsWith("Wiederherstellung erfolgreich", StringComparison.OrdinalIgnoreCase);
if (fileBusy) return;
fileBusy = true;
fileResult = null;
try
{
showRestoreConfirm = false;
fileResult = await BackupService.RestoreBackupAsync();
if (fileResult.Status == FileOperationStatus.Success) await LoadAll();
if (isSuccess)
{
NavigationManager.NavigateTo("/", forceLoad: true);
}
else
{
SetOperationMessage(message, isSuccess);
}
catch (Exception ex) { fileResult = FileOperationResult.Error(ex); }
finally { fileBusy = false; }
}
private async Task HandleDashboardExportAsync()
{
if (fileBusy) return;
fileBusy = true;
fileResult = null;
try
{
var pdf = await PdfStatementService.GenerateDashboardStatementAsync(selectedYear);
var suffix = selectedYear.HasValue ? $"_{selectedYear.Value}" : "_Gesamt";
savedPdfPath = await FileSaveService.SaveFileAsync(pdf, $"{DisplayClubName}_Übersicht{suffix}.pdf");
fileResult = await FileSaveService.SaveFileAsync(pdf, $"{DisplayClubName}_Übersicht{suffix}.pdf");
savedPdfPath = fileResult.Status == FileOperationStatus.Success ? fileResult.Path : null;
}
catch (Exception ex) { fileResult = FileOperationResult.Error(ex); }
finally { fileBusy = false; }
}
private void OpenYearFilterDialog()
@@ -160,12 +176,21 @@ public partial class Dashboard
private async Task HandleOpenSavedPdf()
{
if (fileBusy) return;
fileBusy = true;
fileResult = null;
try
{
if (!string.IsNullOrWhiteSpace(savedPdfPath))
{
await FileSaveService.OpenFileAsync(savedPdfPath);
fileResult = await FileSaveService.OpenFileAsync(savedPdfPath);
}
savedPdfPath = null;
}
catch (Exception ex) { fileResult = FileOperationResult.Error(ex); }
finally { fileBusy = false; }
}
private void CancelOpenSavedPdf()
@@ -177,12 +202,6 @@ public partial class Dashboard
#region Helpers
private void SetOperationMessage(string message, bool success)
{
operationMessage = message;
operationMessageClass = success ? "alert-success" : "alert-danger";
}
private static string FormatCurrency(decimal amount)
{
return amount.ToString("N2", CultureInfo.GetCultureInfo("de-DE")) + " €";
@@ -1,7 +1,9 @@
using Duempelkas.App.Services.Models;
namespace Duempelkas.App.Services;
public interface IBackupService
{
Task<string> CreateBackupAsync();
Task<string> RestoreBackupAsync();
Task<FileOperationResult> CreateBackupAsync();
Task<FileOperationResult> RestoreBackupAsync();
}
@@ -0,0 +1,7 @@
namespace Duempelkas.App.Services;
public interface IFileDialogService
{
Task<string?> SaveAsync(string suggestedFileName);
Task<string?> OpenBackupAsync();
}
@@ -1,7 +1,10 @@
using Duempelkas.App.Services.Models;
namespace Duempelkas.App.Services;
public interface IFileSaveService
{
Task<string?> SaveFileAsync(byte[] content, string suggestedFileName);
Task OpenFileAsync(string filePath);
Task<FileOperationResult> SaveFileAsync(byte[] content, string suggestedFileName);
Task<FileOperationResult> OpenFileAsync(string filePath);
Task<FileOperationResult> OpenFolderAsync(string filePath);
}
@@ -0,0 +1,10 @@
namespace Duempelkas.App.Services.Models;
public enum FileOperationStatus { Success, Cancelled, Error }
public sealed record FileOperationResult(FileOperationStatus Status, string Message, string? Path = null, string? Details = null)
{
public static FileOperationResult Cancelled() => new(FileOperationStatus.Cancelled, "Vorgang abgebrochen. Es wurde nichts geändert.");
public static FileOperationResult Error(Exception error) => new(FileOperationStatus.Error,
"Die Dateiaktion konnte nicht abgeschlossen werden. Bitte den Speicherort und die Zugriffsrechte prüfen oder einen anderen Speicherort wählen.", Details: error.Message);
}