_workspaces = new();
+ private bool _isLoading = true;
protected override async Task OnInitializedAsync()
{
var authState = await AuthStateProvider.GetAuthenticationStateAsync();
- var user = authState.User;
-
- if (user.Identity?.IsAuthenticated == true)
+
+ if (authState.User.Identity?.IsAuthenticated == true)
{
- // Das ist der "sub"-Claim (Subject), den wir als OwnerId in RavenDB nutzen
- _userId = user.FindFirst(c => c.Type == "http://schemas.xmlsoap.org/ws/2005/05/identity/claims/nameidentifier")?.Value
- ?? user.FindFirst("sub")?.Value
- ?? string.Empty;
+ await LoadWorkspaces();
+ }
+ else
+ {
+ _isLoading = false;
+ }
+ }
+
+ private async Task LoadWorkspaces()
+ {
+ _isLoading = true;
+ try
+ {
+ // Dank unseres Services reicht hier ein simpler Aufruf!
+ _workspaces = await DbService.GetWorkspacesForUserAsync();
+ }
+ finally
+ {
+ _isLoading = false;
}
}
}
\ No newline at end of file
diff --git a/Components/Pages/NodeEditor.razor b/Components/Pages/NodeEditor.razor
new file mode 100644
index 0000000..41f6ed9
--- /dev/null
+++ b/Components/Pages/NodeEditor.razor
@@ -0,0 +1,44 @@
+@using ZahlenAnalyse.Web.Models
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ @foreach (var child in Node.Children.ToList())
+ {
+
+ }
+
+
+@code {
+ [Parameter] public DimensionNode Node { get; set; } = default!;
+ [Parameter] public EventCallback OnRemove { get; set; }
+
+ private void AddChild()
+ {
+ Node.Children.Add(new DimensionNode());
+ }
+
+ private void RemoveChild(DimensionNode child)
+ {
+ Node.Children.Remove(child);
+ }
+}
\ No newline at end of file
diff --git a/Components/Pages/WorkspaceCreate.razor b/Components/Pages/WorkspaceCreate.razor
new file mode 100644
index 0000000..eb993f5
--- /dev/null
+++ b/Components/Pages/WorkspaceCreate.razor
@@ -0,0 +1,101 @@
+@page "/workspaces/create"
+@using ZahlenAnalyse.Web.Models
+@using ZahlenAnalyse.Web.Services
+@using Microsoft.AspNetCore.Components.Authorization
+@inject WorkspaceService DbService
+@inject AuthenticationStateProvider AuthStateProvider
+@inject NavigationManager NavManager
+@inject ISnackbar Snackbar
+
+
+ Neuen Workspace erstellen
+
+
+
+
+
+ Analysedimensionen
+
+ @foreach (var dim in _workspace.Dimensions.ToList())
+ {
+
+
+
+
+
+
+
+
+ Hierarchie-Knoten:
+
+ @foreach (var rootNode in dim.Nodes.ToList())
+ {
+
+ }
+
+
+ Haupt-Knoten hinzufügen
+
+
+
+ }
+
+
+ Neue Dimension hinzufügen
+
+
+
+
+
+ Abbrechen
+
+ Workspace speichern
+
+
+
+
+
+@code {
+ private Workspace _workspace = new();
+
+ private async Task SaveWorkspace()
+{
+ if (string.IsNullOrWhiteSpace(_workspace.Name))
+ {
+ Snackbar.Add("Bitte gib dem Workspace einen Namen.", Severity.Warning);
+ return;
+ }
+
+ try
+ {
+ // Wir übergeben nur noch das blanke Formular-Objekt.
+ // Der Service kümmert sich um den Auth-Rest!
+ await DbService.SaveWorkspaceAsync(_workspace);
+
+ Snackbar.Add($"Workspace '{_workspace.Name}' erfolgreich gespeichert!", Severity.Success);
+ NavManager.NavigateTo("/");
+ }
+ catch (Exception ex)
+ {
+ Snackbar.Add($"Fehler beim Speichern: {ex.Message}", Severity.Error);
+ }
+}
+}
\ No newline at end of file
diff --git a/Models/AnalysisFakt.cs b/Models/AnalysisFakt.cs
new file mode 100644
index 0000000..718754c
--- /dev/null
+++ b/Models/AnalysisFakt.cs
@@ -0,0 +1,16 @@
+namespace ZahlenAnalyse.Web.Models;
+
+public record AnalysisFakt: IOwnedEntity, IAuditableEntity
+{
+ public string? Id { get; set; }
+ public string WorkspaceId { get; set; } = string.Empty;
+ public string OwnerId { get; set; } = string.Empty;
+
+ public string CreatedBy { get; set; } = string.Empty;
+ public DateTime CreatedAt { get; set; }
+
+ public DateTime Date { get; set; } = DateTime.UtcNow;
+ public decimal Amount { get; set; }
+
+ public Dictionary Dimensions { get; set; } = new();
+}
\ No newline at end of file
diff --git a/Models/IEntityInterfaces.cs b/Models/IEntityInterfaces.cs
new file mode 100644
index 0000000..97a8448
--- /dev/null
+++ b/Models/IEntityInterfaces.cs
@@ -0,0 +1,12 @@
+namespace ZahlenAnalyse.Web.Models;
+
+public interface IOwnedEntity
+{
+ string OwnerId { get; set; }
+}
+
+public interface IAuditableEntity
+{
+ string CreatedBy { get; set; }
+ DateTime CreatedAt { get; set; }
+}
\ No newline at end of file
diff --git a/Models/Workspace.cs b/Models/Workspace.cs
new file mode 100644
index 0000000..3909ce5
--- /dev/null
+++ b/Models/Workspace.cs
@@ -0,0 +1,27 @@
+namespace ZahlenAnalyse.Web.Models;
+
+public record Workspace: IOwnedEntity, IAuditableEntity
+{
+
+ public string? Id { get; set; }
+ public string Name { get; set; } = string.Empty;
+ public string OwnerId { get; set; } = string.Empty; // Deine Pocket-ID (Sub)
+
+ public string CreatedBy { get; set; } = string.Empty;
+ public DateTime CreatedAt { get; set; }
+
+ public List Dimensions { get; set; } = new();
+}
+
+public record DimensionDefinition
+{
+ public string Name { get; set; } = string.Empty;
+ public List Nodes { get; set; } = new();
+}
+
+public record DimensionNode
+{
+ public string Id { get; set; } = Guid.NewGuid().ToString();
+ public string Name { get; set; } = string.Empty;
+ public List Children { get; set; } = new();
+}
\ No newline at end of file
diff --git a/Program.cs b/Program.cs
index 2f5b8c9..0e99fa6 100644
--- a/Program.cs
+++ b/Program.cs
@@ -6,6 +6,7 @@ using Microsoft.AspNetCore.Authentication.Cookies;
using Microsoft.AspNetCore.Authentication.OpenIdConnect;
using Microsoft.IdentityModel.Tokens;
using DotNetEnv;
+using ZahlenAnalyse.Web.Services;
Env.Load();
@@ -23,6 +24,7 @@ var store = new DocumentStore
};
store.Initialize();
builder.Services.AddSingleton(store);
+builder.Services.AddScoped();
builder.Services.AddCascadingAuthenticationState();
diff --git a/Services/WorkspaceService.cs b/Services/WorkspaceService.cs
new file mode 100644
index 0000000..80d2622
--- /dev/null
+++ b/Services/WorkspaceService.cs
@@ -0,0 +1,100 @@
+using Microsoft.AspNetCore.Components.Authorization;
+using Raven.Client.Documents;
+using ZahlenAnalyse.Web.Models;
+
+namespace ZahlenAnalyse.Web.Services;
+
+public class WorkspaceService
+{
+ private readonly IDocumentStore _store;
+ private readonly AuthenticationStateProvider _authStateProvider;
+
+ // Den AuthStateProvider injizieren
+ public WorkspaceService(IDocumentStore store, AuthenticationStateProvider authStateProvider)
+ {
+ _store = store;
+ _authStateProvider = authStateProvider;
+ }
+
+ private async Task GetUserIdAsync()
+ {
+ var authState = await _authStateProvider.GetAuthenticationStateAsync();
+ var user = authState.User;
+ var userid = user.FindFirst(c => c.Type == "http://schemas.xmlsoap.org/ws/2005/05/identity/claims/nameidentifier")?.Value
+ ?? user.FindFirst("sub")?.Value
+ ?? string.Empty;
+ return userid;
+ }
+
+ // --- Die Magie passiert hier ---
+ private async Task EnrichWithAuditDataAsync(object entity)
+ {
+ // Wenn das Objekt weder IOwnedEntity noch IAuditableEntity ist, können wir abbrechen
+ if (entity is not IOwnedEntity and not IAuditableEntity)
+ return;
+
+ var authState = await _authStateProvider.GetAuthenticationStateAsync();
+ var user = authState.User;
+
+ if (entity is IOwnedEntity ownedEntity)
+ {
+ var userid = await GetUserIdAsync();
+
+ // Setzt bei JEDEM Speichern sicherheitshalber den aktuellen User als Owner
+ ownedEntity.OwnerId = userid;
+ }
+
+ if (entity is IAuditableEntity auditableEntity)
+ {
+ // WICHTIG: Wir setzen CreatedBy und CreatedAt NUR, wenn sie noch leer sind.
+ // Sonst würden wir bei einem Update (z.B. Namensänderung des Workspaces)
+ // das ursprüngliche Erstellungsdatum und den ursprünglichen Ersteller überschreiben!
+ if (string.IsNullOrWhiteSpace(auditableEntity.CreatedBy))
+ {
+ auditableEntity.CreatedBy = user.FindFirst("name")?.Value ?? user.Identity?.Name ?? "Unbekannt";
+ auditableEntity.CreatedAt = DateTime.UtcNow;
+ }
+ }
+ }
+
+ public async Task SaveWorkspaceAsync(Workspace workspace)
+ {
+ // 1. Audit-Daten automatisch befüllen
+ await EnrichWithAuditDataAsync(workspace);
+
+ // 2. Speichern
+ using var session = _store.OpenAsyncSession();
+ await session.StoreAsync(workspace);
+ await session.SaveChangesAsync();
+ }
+
+ public async Task> GetWorkspacesForUserAsync()
+ {
+ // Hier können wir jetzt auch die OwnerId direkt aus dem Token ziehen!
+ // Du musst sie nicht mehr von der UI aus übergeben.
+ var authState = await _authStateProvider.GetAuthenticationStateAsync();
+ var ownerId = await GetUserIdAsync();
+
+ using var session = _store.OpenAsyncSession();
+ return await session.Query()
+ .Where(w => w.OwnerId == ownerId)
+ .ToListAsync();
+ }
+
+ public async Task GetWorkspaceAsync(string id)
+ {
+ var authState = await _authStateProvider.GetAuthenticationStateAsync();
+ var currentUserId = await GetUserIdAsync();
+
+ using var session = _store.OpenAsyncSession();
+ var workspace = await session.LoadAsync(id);
+
+
+ if (workspace != null && workspace.OwnerId != currentUserId)
+ {
+ return null;
+ }
+
+ return workspace;
+ }
+}
\ No newline at end of file