diff --git a/WebApp/Components/Features/Events/CareerMapping.razor b/WebApp/Components/Features/Events/CareerMapping.razor new file mode 100644 index 0000000..108e5bf --- /dev/null +++ b/WebApp/Components/Features/Events/CareerMapping.razor @@ -0,0 +1,146 @@ +@page "/events/career-mapping" +@attribute [Authorize] +@using Microsoft.EntityFrameworkCore +@using WebApp.Components.Shared.Components +@inject AppDbContext Context + + + +@if (_isLoading) +{ + + + Loading career mapping data... + +} +else if (string.IsNullOrWhiteSpace(_mermaidDefinition)) +{ + + No Career Mappings Found + + No events have related careers assigned. Edit events to add related careers. + + +} +else +{ + + Event-Career Relationships + + This diagram shows the connections between events and their related careers. + Events are shown on the left, careers on the right. + +
+ +
+
+} + +@code { + private string _mermaidDefinition = string.Empty; + private bool _isLoading = true; + + protected override async Task OnInitializedAsync() + { + await LoadDataAsync(); + } + + private async Task LoadDataAsync() + { + _isLoading = true; + + try + { + var events = await Context.Events + .Include(e => e.RelatedCareers) + .Where(e => e.RelatedCareers.Any()) + .OrderBy(e => e.Name) + .ToListAsync(); + + if (events.Any()) + { + _mermaidDefinition = GenerateMermaidDiagram(events); + } + } + finally + { + _isLoading = false; + } + } + + private string GenerateMermaidDiagram(List events) + { + var builder = new System.Text.StringBuilder(); + builder.AppendLine("graph LR"); + + // Dictionary to track node IDs and labels (to avoid duplicates) + var eventNodeIds = new Dictionary(); + var careerNodeIds = new Dictionary(); + var careerCounter = 1; + var eventCounter = 1; + + // First pass: collect all unique nodes + foreach (var evt in events) + { + if (!eventNodeIds.ContainsKey(evt.Id)) + { + var eventNodeId = $"E{eventCounter++}"; + var eventLabel = EscapeMermaidLabel(evt.Name); + eventNodeIds[evt.Id] = (eventNodeId, eventLabel); + } + + foreach (var career in evt.RelatedCareers) + { + if (!careerNodeIds.ContainsKey(career.Id)) + { + var careerNodeId = $"C{careerCounter++}"; + var careerLabel = EscapeMermaidLabel(career.Name); + careerNodeIds[career.Id] = (careerNodeId, careerLabel); + } + } + } + + // Second pass: define all nodes + foreach (var (id, (nodeId, label)) in eventNodeIds.OrderBy(e => e.Value.Label)) + { + builder.AppendLine($" {nodeId}[\"{label}\"]"); + } + + foreach (var (id, (nodeId, label)) in careerNodeIds.OrderBy(c => c.Value.Label)) + { + builder.AppendLine($" {nodeId}[\"{label}\"]"); + } + + // Third pass: define all edges + foreach (var evt in events.OrderBy(e => e.Name)) + { + var currentEventNodeId = eventNodeIds[evt.Id].Id; + + foreach (var career in evt.RelatedCareers.OrderBy(c => c.Name)) + { + var currentCareerNodeId = careerNodeIds[career.Id].Id; + builder.AppendLine($" {currentEventNodeId} --> {currentCareerNodeId}"); + } + } + + return builder.ToString(); + } + + private string EscapeMermaidLabel(string label) + { + if (string.IsNullOrEmpty(label)) + return string.Empty; + + // Escape quotes and other special characters for Mermaid labels + return label + .Replace("\"", """) + .Replace("\n", " ") + .Replace("\r", " ") + .Trim(); + } +} + diff --git a/WebApp/Components/Features/Events/Index.razor b/WebApp/Components/Features/Events/Index.razor index 490f8b9..101ea54 100644 --- a/WebApp/Components/Features/Events/Index.razor +++ b/WebApp/Components/Features/Events/Index.razor @@ -11,6 +11,7 @@ Create New Printable Descriptions + Career Mapping
diff --git a/WebApp/Components/_Imports.razor b/WebApp/Components/_Imports.razor index 1e3980e..0ad4ad2 100644 --- a/WebApp/Components/_Imports.razor +++ b/WebApp/Components/_Imports.razor @@ -26,3 +26,4 @@ @using MudBlazor @using Core.Entities @using Data +@using Blazorade.Mermaid.Components diff --git a/WebApp/WebApp.csproj b/WebApp/WebApp.csproj index 06e0b86..6a8184c 100644 --- a/WebApp/WebApp.csproj +++ b/WebApp/WebApp.csproj @@ -14,6 +14,7 @@ +