procedureOptions = new()
+ {
+ new() { Value = 0, Text = "PRTBMY_CATALOG_UPDATE" },
+ new() { Value = 1, Text = "PRTBMY_CATALOG_SAVE" }
+ };
+
+ protected override async Task OnInitializedAsync()
+ {
+ await LoadCatalogs();
+ }
+
+ private async Task LoadCatalogs()
+ {
+ isLoading = true;
+ errorMessage = null;
+ try
+ {
+ items = await Api.GetAllAsync();
+ }
+ catch (Exception ex)
+ {
+ errorMessage = $"Kataloge konnten nicht geladen werden: {ex.Message}";
+ }
+ finally
+ {
+ isLoading = false;
+ StateHasChanged();
+ }
+ }
+
+ private void StartCreate()
+ {
+ formModel = new CatalogWriteDto();
+ editingId = 0;
+ isEditing = false;
+ showForm = true;
+ infoMessage = null;
+ errorMessage = null;
+ }
+
+ private void StartEdit(CatalogReadDto item)
+ {
+ formModel = new CatalogWriteDto
+ {
+ CatTitle = item.CatTitle,
+ CatString = item.CatString,
+ UpdateProcedure = 0
+ };
+ editingId = item.Guid;
+ isEditing = true;
+ showForm = true;
+ infoMessage = null;
+ errorMessage = null;
+ }
+
+ private async Task HandleSubmit()
+ {
+ errorMessage = null;
+ infoMessage = null;
+
+ try
+ {
+ if (isEditing)
+ {
+ var updated = await Api.UpdateAsync(editingId, formModel);
+ if (!updated.Success)
+ {
+ errorMessage = updated.Error ?? "Aktualisierung fehlgeschlagen.";
+ return;
+ }
+
+ infoMessage = "Katalog aktualisiert.";
+ }
+ else
+ {
+ var created = await Api.CreateAsync(formModel);
+ if (!created.Success || created.Value == null)
+ {
+ errorMessage = created.Error ?? "Anlegen fehlgeschlagen.";
+ return;
+ }
+
+ infoMessage = "Katalog angelegt.";
+ }
+
+ showForm = false;
+ await LoadCatalogs();
+ }
+ catch (Exception ex)
+ {
+ errorMessage = $"Fehler beim Speichern: {ex.Message}";
+ }
+ }
+
+ private void CancelEdit()
+ {
+ showForm = false;
+ infoMessage = null;
+ errorMessage = null;
+ }
+
+ private async Task DeleteCatalog(int id)
+ {
+ errorMessage = null;
+ infoMessage = null;
+
+ try
+ {
+ var deleted = await Api.DeleteAsync(id);
+ if (!deleted.Success)
+ {
+ errorMessage = deleted.Error ?? "Löschen fehlgeschlagen.";
+ return;
+ }
+
+ infoMessage = "Katalog gelöscht.";
+ await LoadCatalogs();
+ }
+ catch (Exception ex)
+ {
+ errorMessage = $"Fehler beim Löschen: {ex.Message}";
+ }
+ }
+
+ private sealed class ProcedureOption
+ {
+ public int Value { get; set; }
+ public string Text { get; set; } = string.Empty;
+ }
+}
diff --git a/DbFirst.BlazorWasm/Layout/NavMenu.razor b/DbFirst.BlazorWasm/Layout/NavMenu.razor
index 8c006ab..56037f9 100644
--- a/DbFirst.BlazorWasm/Layout/NavMenu.razor
+++ b/DbFirst.BlazorWasm/Layout/NavMenu.razor
@@ -22,8 +22,8 @@
-
- Web Dashboard
+
+ Dashboards
diff --git a/DbFirst.BlazorWasm/Pages/Catalogs.razor b/DbFirst.BlazorWasm/Pages/Catalogs.razor
index edecb94..fa158c3 100644
--- a/DbFirst.BlazorWasm/Pages/Catalogs.razor
+++ b/DbFirst.BlazorWasm/Pages/Catalogs.razor
@@ -1,303 +1,7 @@
-@* Stellt die Catalog-Verwaltung bereit.
- Nutzt CatalogApiClient für API-Interaktionen und DevExpress-Komponenten für die Benutzeroberfläche. *@
-
@page "/catalogs"
-@inject CatalogApiClient Api
-
-
Catalogs
Catalogs
-@if (!string.IsNullOrWhiteSpace(errorMessage))
-{
- @errorMessage
-}
-else if (!string.IsNullOrWhiteSpace(infoMessage))
-{
- @infoMessage
-}
-
-
- Neuen Eintrag anlegen
-
-
-@if (showForm)
-{
-
-
-
-
-
-
-
-
-
- @if (isEditing)
- {
-
-
-
- }
-
-
- @((isEditing ? "Speichern" : "Anlegen"))
- Abbrechen
-
-
-
-
-
-}
-
-@if (isLoading)
-{
- Lade Daten...
-}
-else if (items.Count == 0)
-{
- Keine Einträge vorhanden.
-}
-else
-{
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- @{ var item = (CatalogReadDto)cell.DataItem; }
-
- Bearbeiten
- Löschen
-
-
-
-
-
-
-}
-
-@code {
- private List items = new();
- private CatalogWriteDto formModel = new();
- private int editingId;
- private bool isLoading;
- private bool isEditing;
- private bool showForm;
- private string? errorMessage;
- private string? infoMessage;
-
- private readonly List procedureOptions = new()
- {
- new() { Value = 0, Text = "PRTBMY_CATALOG_UPDATE" },
- new() { Value = 1, Text = "PRTBMY_CATALOG_SAVE" }
- };
-
- protected override async Task OnInitializedAsync()
- {
- await LoadCatalogs();
- }
-
- private async Task LoadCatalogs()
- {
- // Lädt die Liste der Kataloge aus der API.
- // Setzt Ladezustand und behandelt Fehler.
-
- isLoading = true;
- errorMessage = null;
- try
- {
- items = await Api.GetAllAsync();
- }
- catch (Exception ex)
- {
- errorMessage = $"Kataloge konnten nicht geladen werden: {ex.Message}";
- }
- finally
- {
- isLoading = false;
- StateHasChanged();
- }
- }
-
- private void StartCreate()
- {
- formModel = new CatalogWriteDto();
- editingId = 0;
- isEditing = false;
- showForm = true;
- infoMessage = null;
- errorMessage = null;
- }
-
- private void StartEdit(CatalogReadDto item)
- {
- formModel = new CatalogWriteDto
- {
- CatTitle = item.CatTitle,
- CatString = item.CatString,
- UpdateProcedure = 0
- };
- editingId = item.Guid;
- isEditing = true;
- showForm = true;
- infoMessage = null;
- errorMessage = null;
- }
-
- private async Task HandleSubmit()
- {
- // Behandelt das Absenden des Formulars.
- // Führt entweder eine Aktualisierung oder das Anlegen eines neuen Eintrags durch.
-
- errorMessage = null;
- infoMessage = null;
-
- try
- {
- if (isEditing)
- {
- var updated = await Api.UpdateAsync(editingId, formModel);
- if (!updated.Success)
- {
- errorMessage = updated.Error ?? "Aktualisierung fehlgeschlagen.";
- return;
- }
-
- infoMessage = "Katalog aktualisiert.";
- }
- else
- {
- var created = await Api.CreateAsync(formModel);
- if (!created.Success || created.Value == null)
- {
- errorMessage = created.Error ?? "Anlegen fehlgeschlagen.";
- return;
- }
-
- infoMessage = "Katalog angelegt.";
- }
-
- showForm = false;
- await LoadCatalogs();
- }
- catch (Exception ex)
- {
- errorMessage = $"Fehler beim Speichern: {ex.Message}";
- }
- }
-
- private void CancelEdit()
- {
- showForm = false;
- infoMessage = null;
- errorMessage = null;
- }
-
- private async Task DeleteCatalog(int id)
- {
- // Löscht einen Katalogeintrag basierend auf der ID.
- // Aktualisiert die Liste nach erfolgreichem Löschen.
-
- errorMessage = null;
- infoMessage = null;
-
- try
- {
- var deleted = await Api.DeleteAsync(id);
- if (!deleted.Success)
- {
- errorMessage = deleted.Error ?? "Löschen fehlgeschlagen.";
- return;
- }
-
- infoMessage = "Katalog gelöscht.";
- await LoadCatalogs();
- }
- catch (Exception ex)
- {
- errorMessage = $"Fehler beim Löschen: {ex.Message}";
- }
- }
-
- private sealed class ProcedureOption
- {
- public int Value { get; set; }
- public string Text { get; set; } = string.Empty;
- }
-}
+
diff --git a/DbFirst.BlazorWasm/Pages/Dashboard.razor b/DbFirst.BlazorWasm/Pages/Dashboard.razor
index 0b228d0..242840e 100644
--- a/DbFirst.BlazorWasm/Pages/Dashboard.razor
+++ b/DbFirst.BlazorWasm/Pages/Dashboard.razor
@@ -1,12 +1,87 @@
@page "/dashboard"
+@page "/dashboards/{DashboardId?}"
@inject Microsoft.Extensions.Configuration.IConfiguration Configuration
+@inject NavigationManager Navigation
-
-
+
+
+Dashboards
+
+
+
+ Dashboards
+ Default Dashboard (Designer)
+ Catalogs (Dashboard Grid)
+ Catalogs (Custom Grid)
+
+
+ @if (SelectedDashboardId == "default")
+ {
+
+
+ }
+ else if (SelectedDashboardId == "catalog-grid")
+ {
+
+
+ }
+ else if (SelectedDashboardId == "custom-grid")
+ {
+ Catalogs (Custom Grid)
+
+ }
+
+
@code {
- private string DashboardEndpoint => $"{Configuration["ApiBaseUrl"]?.TrimEnd('/')}/api/dashboard";
-}
+ [Parameter] public string? DashboardId { get; set; }
-@*
- *@
\ No newline at end of file
+ private string DashboardEndpoint => $"{Configuration["ApiBaseUrl"]?.TrimEnd('/')}/api/dashboard";
+ private string SelectedDashboardId => string.IsNullOrWhiteSpace(DashboardId) ? "default" : DashboardId;
+
+ protected override void OnParametersSet()
+ {
+ if (string.IsNullOrWhiteSpace(DashboardId))
+ {
+ Navigation.NavigateTo("dashboards/default", replace: true);
+ }
+ }
+}
\ No newline at end of file
diff --git a/DbFirst.BlazorWasm/_Imports.razor b/DbFirst.BlazorWasm/_Imports.razor
index 27c188c..519e801 100644
--- a/DbFirst.BlazorWasm/_Imports.razor
+++ b/DbFirst.BlazorWasm/_Imports.razor
@@ -10,6 +10,7 @@
@using DbFirst.BlazorWasm.Layout
@using DbFirst.BlazorWasm.Models
@using DbFirst.BlazorWasm.Services
+@using DbFirst.BlazorWasm.Components
@using DevExpress.Blazor
@using DevExpress.DashboardBlazor
@using DevExpress.DashboardWeb
\ No newline at end of file
diff --git a/DbFirst.BlazorWebApp/Components/CatalogsGrid.razor b/DbFirst.BlazorWebApp/Components/CatalogsGrid.razor
new file mode 100644
index 0000000..b3f4c21
--- /dev/null
+++ b/DbFirst.BlazorWebApp/Components/CatalogsGrid.razor
@@ -0,0 +1,286 @@
+@inject CatalogApiClient Api
+
+
+
+@if (!string.IsNullOrWhiteSpace(errorMessage))
+{
+ @errorMessage
+}
+else if (!string.IsNullOrWhiteSpace(infoMessage))
+{
+ @infoMessage
+}
+
+
+ Neuen Eintrag anlegen
+
+
+@if (showForm)
+{
+
+
+
+
+
+
+
+
+
+ @if (isEditing)
+ {
+
+
+
+ }
+
+
+ @((isEditing ? "Speichern" : "Anlegen"))
+ Abbrechen
+
+
+
+
+
+}
+
+@if (isLoading)
+{
+ Lade Daten...
+}
+else if (items.Count == 0)
+{
+ Keine Einträge vorhanden.
+}
+else
+{
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ @{ var item = (CatalogReadDto)cell.DataItem; }
+
+ Bearbeiten
+ Löschen
+
+
+
+
+
+
+}
+
+@code {
+ private List items = new();
+ private CatalogWriteDto formModel = new();
+ private int editingId;
+ private bool isLoading;
+ private bool isEditing;
+ private bool showForm;
+ private string? errorMessage;
+ private string? infoMessage;
+
+ private readonly List procedureOptions = new()
+ {
+ new() { Value = 0, Text = "PRTBMY_CATALOG_UPDATE" },
+ new() { Value = 1, Text = "PRTBMY_CATALOG_SAVE" }
+ };
+
+ protected override async Task OnInitializedAsync()
+ {
+ await LoadCatalogs();
+ }
+
+ private async Task LoadCatalogs()
+ {
+ isLoading = true;
+ errorMessage = null;
+ try
+ {
+ items = await Api.GetAllAsync();
+ }
+ catch (Exception ex)
+ {
+ errorMessage = $"Kataloge konnten nicht geladen werden: {ex.Message}";
+ }
+ finally
+ {
+ isLoading = false;
+ StateHasChanged();
+ }
+ }
+
+ private void StartCreate()
+ {
+ formModel = new CatalogWriteDto();
+ editingId = 0;
+ isEditing = false;
+ showForm = true;
+ infoMessage = null;
+ errorMessage = null;
+ }
+
+ private void StartEdit(CatalogReadDto item)
+ {
+ formModel = new CatalogWriteDto
+ {
+ CatTitle = item.CatTitle,
+ CatString = item.CatString,
+ UpdateProcedure = 0
+ };
+ editingId = item.Guid;
+ isEditing = true;
+ showForm = true;
+ infoMessage = null;
+ errorMessage = null;
+ }
+
+ private async Task HandleSubmit()
+ {
+ errorMessage = null;
+ infoMessage = null;
+
+ try
+ {
+ if (isEditing)
+ {
+ var updated = await Api.UpdateAsync(editingId, formModel);
+ if (!updated.Success)
+ {
+ errorMessage = updated.Error ?? "Aktualisierung fehlgeschlagen.";
+ return;
+ }
+
+ infoMessage = "Katalog aktualisiert.";
+ }
+ else
+ {
+ var created = await Api.CreateAsync(formModel);
+ if (!created.Success || created.Value == null)
+ {
+ errorMessage = created.Error ?? "Anlegen fehlgeschlagen.";
+ return;
+ }
+
+ infoMessage = "Katalog angelegt.";
+ }
+
+ showForm = false;
+ await LoadCatalogs();
+ }
+ catch (Exception ex)
+ {
+ errorMessage = $"Fehler beim Speichern: {ex.Message}";
+ }
+ }
+
+ private void CancelEdit()
+ {
+ showForm = false;
+ infoMessage = null;
+ errorMessage = null;
+ }
+
+ private async Task DeleteCatalog(int id)
+ {
+ errorMessage = null;
+ infoMessage = null;
+
+ try
+ {
+ var deleted = await Api.DeleteAsync(id);
+ if (!deleted.Success)
+ {
+ errorMessage = deleted.Error ?? "Löschen fehlgeschlagen.";
+ return;
+ }
+
+ infoMessage = "Katalog gelöscht.";
+ await LoadCatalogs();
+ }
+ catch (Exception ex)
+ {
+ errorMessage = $"Fehler beim Löschen: {ex.Message}";
+ }
+ }
+
+ private sealed class ProcedureOption
+ {
+ public int Value { get; set; }
+ public string Text { get; set; } = string.Empty;
+ }
+}
diff --git a/DbFirst.BlazorWebApp/Components/Layout/NavMenu.razor b/DbFirst.BlazorWebApp/Components/Layout/NavMenu.razor
index 4363fd0..bae738e 100644
--- a/DbFirst.BlazorWebApp/Components/Layout/NavMenu.razor
+++ b/DbFirst.BlazorWebApp/Components/Layout/NavMenu.razor
@@ -26,8 +26,8 @@
-
- Web Dashboard
+
+ Dashboards
diff --git a/DbFirst.BlazorWebApp/Components/Pages/Dashboard.razor b/DbFirst.BlazorWebApp/Components/Pages/Dashboard.razor
index 0b228d0..242840e 100644
--- a/DbFirst.BlazorWebApp/Components/Pages/Dashboard.razor
+++ b/DbFirst.BlazorWebApp/Components/Pages/Dashboard.razor
@@ -1,12 +1,87 @@
@page "/dashboard"
+@page "/dashboards/{DashboardId?}"
@inject Microsoft.Extensions.Configuration.IConfiguration Configuration
+@inject NavigationManager Navigation
-
-
+
+
+Dashboards
+
+
+
+ Dashboards
+ Default Dashboard (Designer)
+ Catalogs (Dashboard Grid)
+ Catalogs (Custom Grid)
+
+
+ @if (SelectedDashboardId == "default")
+ {
+
+
+ }
+ else if (SelectedDashboardId == "catalog-grid")
+ {
+
+
+ }
+ else if (SelectedDashboardId == "custom-grid")
+ {
+ Catalogs (Custom Grid)
+
+ }
+
+
@code {
- private string DashboardEndpoint => $"{Configuration["ApiBaseUrl"]?.TrimEnd('/')}/api/dashboard";
-}
+ [Parameter] public string? DashboardId { get; set; }
-@*
- *@
\ No newline at end of file
+ private string DashboardEndpoint => $"{Configuration["ApiBaseUrl"]?.TrimEnd('/')}/api/dashboard";
+ private string SelectedDashboardId => string.IsNullOrWhiteSpace(DashboardId) ? "default" : DashboardId;
+
+ protected override void OnParametersSet()
+ {
+ if (string.IsNullOrWhiteSpace(DashboardId))
+ {
+ Navigation.NavigateTo("dashboards/default", replace: true);
+ }
+ }
+}
\ No newline at end of file
diff --git a/DbFirst.BlazorWebApp/Components/_Imports.razor b/DbFirst.BlazorWebApp/Components/_Imports.razor
index 2fc8dc7..f00c849 100644
--- a/DbFirst.BlazorWebApp/Components/_Imports.razor
+++ b/DbFirst.BlazorWebApp/Components/_Imports.razor
@@ -8,6 +8,9 @@
@using Microsoft.JSInterop
@using DbFirst.BlazorWebApp
@using DbFirst.BlazorWebApp.Components
+@using DbFirst.BlazorWebApp.Models
+@using DbFirst.BlazorWebApp.Services
@using DevExpress.Blazor
@using DevExpress.DashboardBlazor
-@using DevExpress.DashboardWeb
\ No newline at end of file
+@using DevExpress.DashboardWeb
+@using DbFirst.BlazorWebApp
\ No newline at end of file
diff --git a/DbFirst.BlazorWebApp/Models/CatalogReadDto.cs b/DbFirst.BlazorWebApp/Models/CatalogReadDto.cs
new file mode 100644
index 0000000..f930e2e
--- /dev/null
+++ b/DbFirst.BlazorWebApp/Models/CatalogReadDto.cs
@@ -0,0 +1,12 @@
+namespace DbFirst.BlazorWebApp.Models;
+
+public class CatalogReadDto
+{
+ public int Guid { get; set; }
+ public string CatTitle { get; set; } = null!;
+ public string CatString { get; set; } = null!;
+ public string AddedWho { get; set; } = null!;
+ public DateTime AddedWhen { get; set; }
+ public string? ChangedWho { get; set; }
+ public DateTime? ChangedWhen { get; set; }
+}
diff --git a/DbFirst.BlazorWebApp/Models/CatalogWriteDto.cs b/DbFirst.BlazorWebApp/Models/CatalogWriteDto.cs
new file mode 100644
index 0000000..1525360
--- /dev/null
+++ b/DbFirst.BlazorWebApp/Models/CatalogWriteDto.cs
@@ -0,0 +1,8 @@
+namespace DbFirst.BlazorWebApp.Models;
+
+public class CatalogWriteDto
+{
+ public string CatTitle { get; set; } = string.Empty;
+ public string CatString { get; set; } = string.Empty;
+ public int UpdateProcedure { get; set; }
+}
diff --git a/DbFirst.BlazorWebApp/Program.cs b/DbFirst.BlazorWebApp/Program.cs
index d244f42..3a2095c 100644
--- a/DbFirst.BlazorWebApp/Program.cs
+++ b/DbFirst.BlazorWebApp/Program.cs
@@ -1,4 +1,5 @@
using DbFirst.BlazorWebApp.Components;
+using DbFirst.BlazorWebApp.Services;
using DevExpress.Blazor;
var builder = WebApplication.CreateBuilder(args);
@@ -9,6 +10,19 @@ builder.Services.AddRazorComponents()
builder.Services.AddDevExpressBlazor();
+var apiBaseUrl = builder.Configuration["ApiBaseUrl"];
+if (!string.IsNullOrWhiteSpace(apiBaseUrl))
+{
+ builder.Services.AddHttpClient(client =>
+ {
+ client.BaseAddress = new Uri(apiBaseUrl);
+ });
+}
+else
+{
+ builder.Services.AddHttpClient();
+}
+
var app = builder.Build();
// Configure the HTTP request pipeline.
diff --git a/DbFirst.BlazorWebApp/Services/CatalogApiClient.cs b/DbFirst.BlazorWebApp/Services/CatalogApiClient.cs
new file mode 100644
index 0000000..1c4590a
--- /dev/null
+++ b/DbFirst.BlazorWebApp/Services/CatalogApiClient.cs
@@ -0,0 +1,136 @@
+using System.Net;
+using System.Net.Http.Json;
+using DbFirst.BlazorWebApp.Models;
+
+namespace DbFirst.BlazorWebApp.Services;
+
+public class CatalogApiClient
+{
+ private readonly HttpClient _httpClient;
+ private const string Endpoint = "api/catalogs";
+
+ public CatalogApiClient(HttpClient httpClient)
+ {
+ _httpClient = httpClient;
+ }
+
+ public async Task> GetAllAsync()
+ {
+ var result = await _httpClient.GetFromJsonAsync>(Endpoint);
+ return result ?? new List();
+ }
+
+ public async Task GetByIdAsync(int id)
+ {
+ return await _httpClient.GetFromJsonAsync($"{Endpoint}/{id}");
+ }
+
+ public async Task> CreateAsync(CatalogWriteDto dto)
+ {
+ var response = await _httpClient.PostAsJsonAsync(Endpoint, dto);
+ if (response.IsSuccessStatusCode)
+ {
+ var payload = await response.Content.ReadFromJsonAsync();
+ return ApiResult.Ok(payload);
+ }
+
+ var error = await ReadErrorAsync(response);
+ return ApiResult.Fail(error);
+ }
+
+ public async Task> UpdateAsync(int id, CatalogWriteDto dto)
+ {
+ var response = await _httpClient.PutAsJsonAsync($"{Endpoint}/{id}", dto);
+ if (response.IsSuccessStatusCode)
+ {
+ return ApiResult.Ok(true);
+ }
+
+ var error = await ReadErrorAsync(response);
+ return ApiResult.Fail(error);
+ }
+
+ public async Task> DeleteAsync(int id)
+ {
+ var response = await _httpClient.DeleteAsync($"{Endpoint}/{id}");
+ if (response.IsSuccessStatusCode)
+ {
+ return ApiResult.Ok(true);
+ }
+
+ var error = await ReadErrorAsync(response);
+ return ApiResult.Fail(error);
+ }
+
+ private static async Task ReadErrorAsync(HttpResponseMessage response)
+ {
+ string? problemTitle = null;
+ string? problemDetail = null;
+
+ try
+ {
+ var problem = await response.Content.ReadFromJsonAsync();
+ if (problem != null)
+ {
+ problemTitle = problem.Title;
+ problemDetail = problem.Detail ?? problem.Type;
+ }
+ }
+ catch
+ {
+ }
+
+ var status = response.StatusCode;
+ var reason = response.ReasonPhrase;
+ var body = await response.Content.ReadAsStringAsync();
+
+ string? detail = problemDetail;
+ if (string.IsNullOrWhiteSpace(detail) && !string.IsNullOrWhiteSpace(body))
+ {
+ detail = body;
+ }
+
+ if (status == HttpStatusCode.Conflict)
+ {
+ return "Datensatz existiert bereits. Bitte wählen Sie einen anderen Titel.";
+ }
+ if (status == HttpStatusCode.BadRequest && (detail?.Contains("CatTitle cannot be changed", StringComparison.OrdinalIgnoreCase) ?? false))
+ {
+ return "Titel kann nicht geändert werden.";
+ }
+
+ return status switch
+ {
+ HttpStatusCode.BadRequest => $"Eingabe ungültig{FormatSuffix(problemTitle, detail, reason)}",
+ HttpStatusCode.NotFound => $"Nicht gefunden{FormatSuffix(problemTitle, detail, reason)}",
+ HttpStatusCode.Conflict => $"Konflikt{FormatSuffix(problemTitle, detail, reason)}",
+ HttpStatusCode.Unauthorized => $"Nicht autorisiert{FormatSuffix(problemTitle, detail, reason)}",
+ HttpStatusCode.Forbidden => $"Nicht erlaubt{FormatSuffix(problemTitle, detail, reason)}",
+ HttpStatusCode.InternalServerError => $"Serverfehler{FormatSuffix(problemTitle, detail, reason)}",
+ _ => $"Fehler {(int)status} {reason ?? string.Empty}{FormatSuffix(problemTitle, detail, reason)}"
+ };
+ }
+
+ private static string FormatSuffix(string? title, string? detail, string? reason)
+ {
+ var parts = new List();
+ if (!string.IsNullOrWhiteSpace(title)) parts.Add(title);
+ if (!string.IsNullOrWhiteSpace(detail)) parts.Add(detail);
+ if (parts.Count == 0 && !string.IsNullOrWhiteSpace(reason)) parts.Add(reason);
+ if (parts.Count == 0) return string.Empty;
+ return ": " + string.Join(" | ", parts);
+ }
+}
+
+public record ApiResult(bool Success, T? Value, string? Error)
+{
+ public static ApiResult Ok(T? value) => new(true, value, null);
+ public static ApiResult Fail(string? error) => new(false, default, error);
+}
+
+internal sealed class ProblemDetailsDto
+{
+ public string? Type { get; set; }
+ public string? Title { get; set; }
+ public string? Detail { get; set; }
+}