DMS Layout mit Fehlern
All checks were successful
Build & Deploy PLDpro.Web Test to 192.168.1.100 / build-and-deploy (push) Successful in 1m15s

This commit is contained in:
2026-02-09 21:50:24 +01:00
parent f9fb791dca
commit cae77ef1e3
12 changed files with 709 additions and 1 deletions

View File

@@ -7,6 +7,10 @@
<MudNavLink Href="test" Match="NavLinkMatch.Prefix" Icon="@Icons.Material.Filled.List">Test</MudNavLink>
<MudNavLink Href="storage" Match="NavLinkMatch.Prefix" Icon="@Icons.Material.Filled.Cloud">Storage</MudNavLink>
<MudNavLink Href="/dms" Match="NavLinkMatch.Prefix" Icon="@Icons.Material.Filled.Dashboard">DMS</MudNavLink>
<MudNavLink Href="/dms/list" Icon="@Icons.Material.Filled.List">Dokumente</MudNavLink>
<MudNavLink Href="/dms/upload" Icon="@Icons.Material.Filled.Upload">Upload</MudNavLink>
</MudNavMenu>

View File

@@ -0,0 +1,167 @@
@page "/dms"
@using MudBlazor
@using Pldpro.Web.UI.Models
@inject Pldpro.Web.UI.Services.IDocumentClient Client
@inject NavigationManager Nav
<PageTitle>DMS</PageTitle>
<MudStack Spacing="3">
<!-- Kopfzeile: Titel, Bucket-Auswahl und Aktionen -->
<MudStack Row="true" AlignItems=AlignItems.Center Spacing="2">
<MudText Typo="Typo.h4">Dokumenten-Management</MudText>
<MudSpacer />
<MudSelect T="string" @bind-Value="_bucket" Label="Bucket" Dense="true" Style="min-width:240px">
@foreach (var b in _buckets)
{
<MudSelectItem Value="@b">@b</MudSelectItem>
}
</MudSelect>
<MudButton Variant="Variant.Outlined" OnClick="Reload" StartIcon="@Icons.Material.Filled.Refresh">
Aktualisieren
</MudButton>
<MudButton Variant="Variant.Filled" Color="Color.Primary"
OnClick="@(() => Nav.NavigateTo("/dms/upload"))"
StartIcon="@Icons.Material.Filled.CloudUpload">
Upload
</MudButton>
<MudButton Variant="Variant.Outlined"
OnClick="@(() => Nav.NavigateTo("/dms/list"))"
StartIcon="@Icons.Material.Filled.List">
Zur Liste
</MudButton>
</MudStack>
<!-- Kennzahlen -->
<MudGrid>
<MudItem xs="12" md="3">
<MudPaper Class="pa-4">
<MudText Typo="Typo.subtitle1">Gesamt</MudText>
<MudText Typo="Typo.h5">@_total.ToString("N0")</MudText>
</MudPaper>
</MudItem>
<MudItem xs="12" md="3">
<MudPaper Class="pa-4">
<MudText Typo="Typo.subtitle1">Eingegangen</MudText>
<MudText Typo="Typo.h5">@_eingegangen.ToString("N0")</MudText>
</MudPaper>
</MudItem>
<MudItem xs="12" md="3">
<MudPaper Class="pa-4">
<MudText Typo="Typo.subtitle1">Freigegeben</MudText>
<MudText Typo="Typo.h5">@_freigegeben.ToString("N0")</MudText>
</MudPaper>
</MudItem>
<MudItem xs="12" md="3">
<MudPaper Class="pa-4">
<MudText Typo="Typo.subtitle1">Bezahlt</MudText>
<MudText Typo="Typo.h5">@_bezahlt.ToString("N0")</MudText>
</MudPaper>
</MudItem>
</MudGrid>
<!-- Zuletzt hinzugefügt + Filter -->
<MudPaper Class="pa-4">
<MudStack Row="true" AlignItems=AlignItems.Center Spacing="2">
<MudText Typo="Typo.h6">Zuletzt hinzugefügt</MudText>
<MudSpacer />
<MudTextField @bind-Value="_query"
Placeholder="Suche (optional)"
Adornment="Adornment.Start"
AdornmentIcon="@Icons.Material.Filled.Search"
Immediate="true" />
<MudTextField @bind-Value="_prefix" Placeholder="Pfad-Prefix (optional, z.B. rechnungen/2026)" />
<MudButton Variant="Variant.Outlined" OnClick="Reload" StartIcon="@Icons.Material.Filled.Search">
Filtern
</MudButton>
</MudStack>
<MudTable Items="_latest" Dense="true" Hover="true" Class="mt-3">
<HeaderContent>
<MudTh>Datei</MudTh>
<MudTh>Pfad</MudTh>
<MudTh>Größe</MudTh>
<MudTh>Geändert</MudTh>
<MudTh></MudTh>
</HeaderContent>
<RowTemplate>
<MudTd>@context.FileName</MudTd>
<MudTd>@context.PathPrefix</MudTd>
<MudTd>@(context.Size?.ToString("N0"))</MudTd>
<MudTd>@context.LastModified</MudTd>
<MudTd Align="TableCellAlign.Right">
<MudTooltip Text="Details">
<MudIconButton Icon="@Icons.Material.Filled.Description"
OnClick="@(() => Nav.NavigateTo($"/dms/detail/{Uri.EscapeDataString(context.Bucket)}/{EncodeKeyForPath(context.Key)}"))" />
</MudTooltip>
<MudTooltip Text="Download">
<MudIconButton Icon="@Icons.Material.Filled.Download"
OnClick="@(() => Download(context))" />
</MudTooltip>
</MudTd>
</RowTemplate>
</MudTable>
</MudPaper>
</MudStack>
@code {
private List<string> _buckets = new();
private string? _bucket;
private string? _query;
private string? _prefix;
private int _total;
private int _eingegangen;
private int _freigegeben;
private int _bezahlt;
private List<DocumentListItem> _latest = new();
protected override async Task OnInitializedAsync()
{
_buckets = (await Client.ListBucketsAsync()).ToList();
_bucket = _buckets.FirstOrDefault();
await Reload();
}
private async Task Reload()
{
if (string.IsNullOrWhiteSpace(_bucket))
{
_total = _eingegangen = _freigegeben = _bezahlt = 0;
_latest = new();
StateHasChanged();
return;
}
// Kennzahlen: einmal komplette Liste (für kleine Datenmengen ok)
var (allItems, total) = await Client.SearchAsync(_bucket!, _query, _prefix, 0, int.MaxValue);
_total = total;
// Aktuell sind Status nur UI-intern; wenn später persistiert, hier echte Counts laden.
_eingegangen = allItems.Count(i => i.Status == DocumentStatus.Eingegangen);
_freigegeben = allItems.Count(i => i.Status == DocumentStatus.Freigegeben);
_bezahlt = allItems.Count(i => i.Status == DocumentStatus.Bezahlt);
// Letzte 10
var (latest, _) = await Client.SearchAsync(_bucket!, _query, _prefix, 0, 10);
_latest = latest
.OrderByDescending(i => i.LastModified ?? DateTime.MinValue)
.Take(10)
.ToList();
StateHasChanged();
}
private void Download(DocumentListItem item)
{
var url = Client.GetDownloadUrl(item.Bucket, item.Key);
Nav.NavigateTo(url, forceLoad: true);
}
private static string EncodeKeyForPath(string key)
=> string.Join("/", (key ?? string.Empty)
.Split('/', StringSplitOptions.RemoveEmptyEntries)
.Select(Uri.EscapeDataString));
}

View File

@@ -0,0 +1,159 @@
@page "/dms/detail/{Bucket}/{*Key}"
@using MudBlazor
@using Pldpro.Web.UI.Models
@inject Pldpro.Web.UI.Services.IDocumentClient Client
@inject NavigationManager Nav
@inject IDialogService Dialogs
@inject ISnackbar Snackbar
<PageTitle>Dokument</PageTitle>
@if (_loading)
{
<MudStack AlignItems=AlignItems.Center Justify=Justify.Center Class="pa-6">
<MudProgressCircular Indeterminate="true" />
</MudStack>
}
else if (_doc is null)
{
<MudPaper Class="pa-4">
<MudAlert Severity="Severity.Error">
Dokument nicht gefunden.
</MudAlert>
<MudButton Variant="Variant.Outlined" OnClick="Back" Class="mt-3">Zurück</MudButton>
</MudPaper>
}
else
{
<MudStack Spacing="2">
<!-- Kopfzeile -->
<MudStack Row="true" AlignItems=AlignItems.Center Spacing="2">
<MudText Typo="Typo.h5">@_doc.FileName</MudText>
<MudSpacer />
<MudButton Variant="Variant.Outlined"
StartIcon="@Icons.Material.Filled.ArrowBack"
OnClick="Back">
Zurück
</MudButton>
</MudStack>
<MudGrid>
<!-- Vorschau (Platzhalter) -->
<MudItem xs="12" md="7">
<MudPaper Class="pa-3" Elevation="1">
<MudText Typo="Typo.subtitle1">Vorschau</MudText>
<MudDivider Class="my-2" />
<MudAlert Severity="Severity.Info">
PDFVorschau Platzhalter später PDF.js/Viewer integrieren.
</MudAlert>
</MudPaper>
</MudItem>
<!-- Details & Aktionen -->
<MudItem xs="12" md="5">
<MudPaper Class="pa-3" Elevation="1">
<MudText Typo="Typo.subtitle1">Details</MudText>
<MudDivider Class="my-2" />
<!-- Nicht-generische Tabelle -->
<MudSimpleTable Dense="true">
<thead>
<tr>
<th>Feld</th>
<th>Wert</th>
</tr>
</thead>
<tbody>
<tr><td>Bucket</td><td>@_doc.Bucket</td></tr>
<tr><td>Key</td><td>@_doc.Key</td></tr>
<tr><td>Dateiname</td><td>@_doc.FileName</td></tr>
<tr><td>Pfad</td><td>@_doc.PathPrefix</td></tr>
<tr><td>Größe</td><td>@(_doc.Size?.ToString("N0")) Bytes</td></tr>
<tr><td>Geändert</td><td>@_doc.LastModified</td></tr>
</tbody>
</MudSimpleTable>
<MudStack Row="true" Spacing="1" Class="mt-3">
<MudButton Color="Color.Primary"
Variant="Variant.Filled"
StartIcon="@Icons.Material.Filled.Download"
OnClick="Download">
Download
</MudButton>
<MudButton Color="Color.Error"
Variant="Variant.Text"
StartIcon="@Icons.Material.Filled.Delete"
OnClick="Delete">
Löschen
</MudButton>
</MudStack>
</MudPaper>
</MudItem>
</MudGrid>
</MudStack>
}
@code {
[Parameter] public string Bucket { get; set; } = default!;
[Parameter] public string Key { get; set; } = default!; // Catch-all wird von Blazor decodiert
private DocumentDetail? _doc;
private bool _loading;
protected override async Task OnParametersSetAsync()
{
_loading = true;
try
{
_doc = await Client.GetAsync(Bucket, Key);
}
catch (Exception ex)
{
Snackbar.Add($"Fehler beim Laden: {ex.Message}", Severity.Error);
_doc = null;
}
finally
{
_loading = false;
}
}
private void Back() => Nav.NavigateTo("/dms/list");
private Task Download()
{
if (_doc is null) return Task.CompletedTask;
var url = Client.GetDownloadUrl(_doc.Bucket, _doc.Key);
Nav.NavigateTo(url, forceLoad: true);
return Task.CompletedTask;
}
private async Task Delete()
{
if (_doc is null) return;
// Du kannst alternativ ShowMessageBox verwenden, wenn du keinen eigenen ConfirmDialog nutzen willst:
var confirm = await Dialogs.ShowMessageBox(
title: "Dokument löschen",
markupMessage: (MarkupString)$"Möchten Sie '<b>{_doc.FileName}</b>' endgültig löschen?",
yesText: "Löschen",
cancelText: "Abbrechen",
options: new DialogOptions { CloseOnEscapeKey = true });
if (confirm == true)
{
try
{
await Client.DeleteAsync(_doc.Bucket, _doc.Key);
Snackbar.Add("Dokument gelöscht.", Severity.Success);
Nav.NavigateTo("/dms/list");
}
catch (Exception ex)
{
Snackbar.Add($"Löschen fehlgeschlagen: {ex.Message}", Severity.Error);
}
}
}
}

View File

@@ -0,0 +1,94 @@
@page "/dms/list"
@using MudBlazor
@using Pldpro.Web.UI.Models
@inject Pldpro.Web.UI.Services.IDocumentClient Client
@inject NavigationManager Nav
<MudPaper Class="pa-4">
<MudStack Row="true" Spacing="2" AlignItems=AlignItems.Center>
<MudSelect T="string" @bind-Value="_bucket" Label="Bucket" Dense="true" Style="min-width:220px" Required="true">
@foreach (var b in _buckets)
{
<MudSelectItem Value="@b">@b</MudSelectItem>
}
</MudSelect>
<MudTextField @bind-Value="_prefix" Placeholder="Pfad-Prefix (optional, z.B. rechnungen/2026)" />
<MudTextField @bind-Value="_query" Placeholder="Suche (Datei/Ordner)"
Adornment="Adornment.Start" AdornmentIcon="@Icons.Material.Filled.Search" Immediate="true" />
<MudSpacer />
<MudButton Variant="Variant.Outlined" OnClick="Reload" StartIcon="@Icons.Material.Filled.Search">Suchen</MudButton>
<MudButton Variant="Variant.Filled" OnClick="@(() => Nav.NavigateTo("/dms/upload"))"
StartIcon="@Icons.Material.Filled.CloudUpload">Upload</MudButton>
</MudStack>
<!-- WICHTIG: T="DocumentListItem" angeben -->
<MudTable T="DocumentListItem" Items="_items" Dense="true" Hover="true" Class="mt-3">
<HeaderContent>
<MudTh>Datei</MudTh>
<MudTh>Bucket</MudTh>
<MudTh>Pfad</MudTh>
<MudTh>Größe</MudTh>
<MudTh>Geändert</MudTh>
<MudTh></MudTh>
</HeaderContent>
<!-- @context ist vom Typ DocumentListItem -->
<RowTemplate>
<MudTd DataLabel="Datei">@context.FileName</MudTd>
<MudTd DataLabel="Bucket">@context.Bucket</MudTd>
<MudTd DataLabel="Pfad">@context.PathPrefix</MudTd>
<MudTd DataLabel="Größe">@context.Size?.ToString("N0")</MudTd>
<MudTd DataLabel="Geändert">@context.LastModified</MudTd>
<MudTd Align="TableCellAlign.Right">
<MudButton Variant="Variant.Text"
OnClick="@(() => Nav.NavigateTo($"/dms/detail/{Uri.EscapeDataString(context.Bucket)}/{EncodeKeyForPath(context.Key)}"))">
Details
</MudButton>
</MudTd>
</RowTemplate>
<PagerContent>
<MudTablePager PageSizeOptions="new int[] { 10, 25, 50 }" @bind-PageSize="_pageSize" />
</PagerContent>
</MudTable>
<MudStack Row="true" Spacing="1" AlignItems=AlignItems.Center Justify=Justify.Center Class="mt-2">
<MudButton Variant="Variant.Outlined" Disabled="@(_page == 0)" OnClick="Prev">Zurück</MudButton>
<MudText>Seite @(_page + 1)</MudText>
<MudButton Variant="Variant.Outlined" Disabled="@(((_page + 1) * _pageSize) >= _total)" OnClick="Next">Weiter</MudButton>
</MudStack>
</MudPaper>
@code {
private List<string> _buckets = new();
private string? _bucket;
private string? _prefix;
private string? _query;
private int _page = 0, _pageSize = 25, _total = 0;
private List<DocumentListItem> _items = new();
protected override async Task OnInitializedAsync()
{
_buckets = (await Client.ListBucketsAsync()).ToList();
_bucket = _buckets.FirstOrDefault();
if (!string.IsNullOrWhiteSpace(_bucket))
await Reload();
}
private async Task Reload()
{
if (string.IsNullOrWhiteSpace(_bucket)) return;
var (items, total) = await Client.SearchAsync(_bucket!, _query, _prefix, _page, _pageSize);
_items = items.ToList(); // <- Items ist List<DocumentListItem>
_total = total;
}
private async Task Prev() { if (_page > 0) { _page--; await Reload(); } }
private async Task Next() { if (((_page + 1) * _pageSize) < _total) { _page++; await Reload(); } }
private static string EncodeKeyForPath(string key)
=> string.Join("/", (key ?? string.Empty)
.Split('/', StringSplitOptions.RemoveEmptyEntries)
.Select(Uri.EscapeDataString));
}

View File

@@ -0,0 +1,94 @@
@page "/dms/upload"
@using MudBlazor
@inject Pldpro.Web.UI.Services.IDocumentClient Client
@inject NavigationManager Nav
@inject ISnackbar Snackbar
<PageTitle>Upload</PageTitle>
<MudPaper Class="pa-4">
<MudText Typo="Typo.h5" GutterBottom="true">Dokument-Upload</MudText>
<MudStack Spacing="2">
<MudSelect T="string" @bind-Value="_bucket" Label="Bucket" Dense="true" Style="min-width:260px" Required="true" RequiredError="Bitte Bucket wählen">
@foreach (var b in _buckets)
{
<MudSelectItem Value="@b">@b</MudSelectItem>
}
</MudSelect>
<MudTextField @bind-Value="_path"
Placeholder="Pfad (optional, z.B. rechnungen/2026)"
Variant="Variant.Outlined"
Adornment="Adornment.Start"
AdornmentIcon="@Icons.Material.Filled.Folder" />
<InputFile OnChange="OnFilesSelected" multiple />
<MudList T="string" Dense="true">
@foreach (var msg in _messages)
{
<MudListItem>@msg</MudListItem>
}
</MudList>
<MudStack Row="true" Spacing="2">
<MudButton Variant="Variant.Outlined" StartIcon="@Icons.Material.Filled.List" OnClick="@(() => Nav.NavigateTo("/dms/list"))">
Zur Liste
</MudButton>
<MudButton Variant="Variant.Outlined" StartIcon="@Icons.Material.Filled.Dashboard" OnClick="@(() => Nav.NavigateTo("/dms"))">
Dashboard
</MudButton>
</MudStack>
</MudStack>
</MudPaper>
@code {
private List<string> _buckets = new();
private string? _bucket;
private string? _path;
private RenderFragment? _result;
private const long StreamLimit = 512L * 1024 * 1024;
protected override async Task OnInitializedAsync()
{
try
{
_buckets = (await Client.ListBucketsAsync()).ToList();
_bucket = _buckets.FirstOrDefault();
}
catch (Exception ex)
{
Snackbar.Add($"Buckets konnten nicht geladen werden: {ex.Message}", Severity.Error);
}
}
private readonly List<string> _messages = new();
private async Task OnFilesSelected(InputFileChangeEventArgs e)
{
if (string.IsNullOrWhiteSpace(_bucket))
{
Snackbar.Add("Bitte zuerst einen Bucket auswählen.", Severity.Warning);
return;
}
foreach (var f in e.GetMultipleFiles())
{
try
{
await Client.UploadAsync(_bucket!, _path, f, StreamLimit);
_messages.Add($"hochgeladen: {f.Name} ({f.Size:N0} Bytes)");
}
catch (Exception ex)
{
_messages.Add($"Fehler bei '{f.Name}': {ex.Message}");
}
}
}
}

View File

@@ -0,0 +1,18 @@
@using MudBlazor
<MudDialog>
<DialogContent>
<MudText Typo="Typo.h6">@Title</MudText>
<MudText Class="mt-2">@Message</MudText>
</DialogContent>
<DialogActions>
<MudButton OnClick="() => MudDialog.Cancel()" Variant="Variant.Outlined" Color="Color.Default">Abbrechen</MudButton>
<MudButton OnClick="() => MudDialog.Close(DialogResult.Ok(true))" Variant="Variant.Filled" Color="Color.Error">OK</MudButton>
</DialogActions>
</MudDialog>
@code {
[CascadingParameter] IMudDialogInstance MudDialog { get; set; } = default!;
[Parameter] public string Title { get; set; } = "Bestätigen";
[Parameter] public string Message { get; set; } = "Sind Sie sicher?";
}
``

View File

@@ -0,0 +1,32 @@
@using Pldpro.Web.UI.Models
@using MudBlazor
<MudChip T="string"
Value="@Text"
Color="@Color"
Variant="Variant.Filled"
Label="true"
DisableRipple="true"
Clickable="false">
@Text
</MudChip>
@code {
[Parameter] public DocumentStatus Status { get; set; }
private string Text => Status switch
{
DocumentStatus.Eingegangen => "Eingegangen",
DocumentStatus.Freigegeben => "Freigegeben",
DocumentStatus.Bezahlt => "Bezahlt",
_ => Status.ToString()
};
private Color Color => Status switch
{
DocumentStatus.Eingegangen => Color.Info,
DocumentStatus.Freigegeben => Color.Success,
DocumentStatus.Bezahlt => Color.Primary,
_ => Color.Default
};
}