diff --git a/Components/Layout/MainLayout.razor b/Components/Layout/MainLayout.razor index acfbe87..3b183b7 100644 --- a/Components/Layout/MainLayout.razor +++ b/Components/Layout/MainLayout.razor @@ -4,6 +4,7 @@ + diff --git a/Components/Pages/Home.razor b/Components/Pages/Home.razor index d3a54a3..abc398e 100644 --- a/Components/Pages/Home.razor +++ b/Components/Pages/Home.razor @@ -1,40 +1,133 @@ @page "/" +@using ZahlenAnalyse.Web.Models +@using ZahlenAnalyse.Web.Services @using Microsoft.AspNetCore.Components.Authorization +@inject WorkspaceService DbService @inject AuthenticationStateProvider AuthStateProvider -Zahlen-Analyse - - - - Willkommen zurück, @context.User.Identity?.Name! - Deine Pocket-ID (Sub): @_userId + + + Meine Workspaces - - Abmelden - - - - Bitte melde dich an, um deine Workspaces zu verwalten. - - Mit Pocket-ID anmelden - - - + + + + Neuer Workspace + + + + + + + + @if (_isLoading) + { + + } + else if (!_workspaces.Any()) + { + + + Noch keine Workspaces vorhanden + + Erstelle deinen ersten Workspace (z.B. Urlaubsabrechnung), um mit der Datenanalyse zu beginnen. + + + Jetzt erstellen + + + } + else + { + + @foreach (var ws in _workspaces) + { + + + + + @ws.Name + + Erstellt am @ws.CreatedAt.ToLocalTime().ToString("dd.MM.yyyy") + + + + + + + + + @ws.Dimensions.Count Dimensionen konfiguriert + + + @foreach (var dim in ws.Dimensions.Take(3)) + { + @dim.Name + } + @if (ws.Dimensions.Count > 3) + { + +@(ws.Dimensions.Count - 3) + } + + + + + Daten erfassen + + + Auswertung + + + + + } + + } + + + + Willkommen beim Zahlen-Analyse Tool + Bitte melde dich an, um deine Daten zu verwalten. + + Mit Pocket-ID anmelden + + + + + @code { - private string _userId = string.Empty; + private List _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