@page "/events/career-mapping" @attribute [Authorize] @using Microsoft.EntityFrameworkCore @using Core.Utility @using VisNetwork.Blazor.Models @using Edge = VisNetwork.Blazor.Models.Edge @inject AppDbContext Context @if (_isLoading) { Loading career mapping data... } else if (_networkData == null || !_networkData.Nodes.Any()) { No Career Field Mappings Found No events have related careers assigned that match any career fields. Edit events to add related careers. } else { Event-Career Field Relationships This diagram shows the connections between events and their related career fields. Events are shown in blue, career fields in green. Career fields are clusters of related careers. Click on a node to see details. Use mouse to zoom and pan the graph. @if (_selectedNodeInfo != null) { @_selectedNodeInfo.Title @if (!string.IsNullOrWhiteSpace(_selectedNodeInfo.Description)) { @_selectedNodeInfo.Description } @if (_selectedNodeInfo.Careers != null && _selectedNodeInfo.Careers.Any()) { Related Careers: @foreach (var career in _selectedNodeInfo.Careers.OrderBy(c => c)) { @career } } }
} @code { private NetworkData? _networkData; private bool _isLoading = true; private Dictionary> _fieldIdToCareers = new(); private List? _allEvents; private Dictionary _nodeIdToFieldId = new(); private Dictionary _nodeIdToEventId = new(); private SelectedNodeInfo? _selectedNodeInfo; private class SelectedNodeInfo { public string Title { get; set; } = string.Empty; public string? Description { get; set; } public bool IsCareerField { get; set; } public List? Careers { get; set; } } 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(); // Filter to only events that have career fields (after matching) var eventsWithFields = events .Where(e => CareerFieldDefinitions.GetRelatedCareerFields(e.RelatedCareers).Any()) .ToList(); _allEvents = eventsWithFields; // Build mapping of career field IDs to their related careers _fieldIdToCareers.Clear(); foreach (var evt in eventsWithFields) { var relatedFields = CareerFieldDefinitions.GetRelatedCareerFields(evt.RelatedCareers); foreach (var field in relatedFields) { if (!_fieldIdToCareers.ContainsKey(field.Id)) { _fieldIdToCareers[field.Id] = new List(); } // Add unique career names for this field foreach (var career in evt.RelatedCareers) { var normalizedCareerName = CareerNormalizer.GetNormalizedKey(career.Name); // Check if this career matches the field var matchesField = field.DirectCareerMatches.Any(dcm => string.Equals(CareerNormalizer.GetNormalizedKey(dcm), normalizedCareerName, StringComparison.OrdinalIgnoreCase)) || field.PatternKeywords.Any(pk => normalizedCareerName.Contains(pk, StringComparison.OrdinalIgnoreCase)); if (matchesField && !_fieldIdToCareers[field.Id].Contains(career.Name, StringComparer.OrdinalIgnoreCase)) { _fieldIdToCareers[field.Id].Add(career.Name); } } } } if (eventsWithFields.Any()) { // Clear mappings before regenerating _nodeIdToFieldId.Clear(); _nodeIdToEventId.Clear(); _networkData = GenerateNetworkData(eventsWithFields); } } finally { _isLoading = false; } } private NetworkData GenerateNetworkData(List events) { var nodes = new List(); var edges = new List(); // Dictionary to track node IDs (to avoid duplicates) var eventNodeIds = new Dictionary(); var fieldNodeIds = new Dictionary(); var eventCounter = 1; var fieldCounter = 1; // Dictionary to track which events connect to which career fields var eventToFields = new Dictionary>(); // First pass: collect all unique nodes and determine relationships foreach (var evt in events.OrderBy(e => e.Name)) { if (!eventNodeIds.ContainsKey(evt.Id)) { var eventNodeId = $"E{eventCounter++}"; eventNodeIds[evt.Id] = eventNodeId; // Add event node (blue) nodes.Add(new Node { Id = eventNodeId, Label = evt.Name, Color = new NodeColorType { Background = "#90C3F5", Border = "#7DB3F0" }, Shape = "box", Size = 25 }); // Store mapping for click handling _nodeIdToEventId[eventNodeId] = evt.Id; } // Get related career fields for this event's careers var relatedFields = CareerFieldDefinitions.GetRelatedCareerFields(evt.RelatedCareers); if (relatedFields.Any()) { eventToFields[evt.Id] = new HashSet(relatedFields); // Track and add career field nodes foreach (var field in relatedFields) { if (!fieldNodeIds.ContainsKey(field.Id)) { var fieldNodeId = $"F{fieldCounter++}"; fieldNodeIds[field.Id] = fieldNodeId; // Add career field node (green) nodes.Add(new Node { Id = fieldNodeId, Label = field.Name, Color = new NodeColorType { Background = "#90F8B0", Border = "#80E8A0", Highlight = new NodeColorType.BorderBackgroundColor { Background = "#90F8B0", Border = "#80E8A0" } }, Shape = "box", Size = 20 }); // Store mapping for click handling _nodeIdToFieldId[fieldNodeId] = field.Id; } } } } // Second pass: create edges from events to career fields foreach (var evt in events.OrderBy(e => e.Name)) { if (eventToFields.TryGetValue(evt.Id, out var fields)) { var eventNodeId = eventNodeIds[evt.Id]; foreach (var field in fields.OrderBy(f => f.Name)) { var fieldNodeId = fieldNodeIds[field.Id]; edges.Add(new Edge { From = eventNodeId, To = fieldNodeId }); } } } return new NetworkData { Nodes = nodes, Edges = edges }; } private NetworkOptions GetNetworkOptions(Network network) { return new NetworkOptions { AutoResize = true, Physics = new PhysicsOptions { Enabled = true }, Height = "600px" }; } private void HandleNetworkClick(ClickEvent eventArg) { _selectedNodeInfo = null; // Check if a node was clicked if (eventArg.Nodes != null && eventArg.Nodes.Count > 0) { var nodeId = eventArg.Nodes[0]; // Check if it's a career field node if (_nodeIdToFieldId.TryGetValue(nodeId, out var fieldId)) { var field = CareerFieldDefinitions.GetAllCareerFields().FirstOrDefault(f => f.Id == fieldId); if (field != null) { _fieldIdToCareers.TryGetValue(fieldId, out var careers); _selectedNodeInfo = new SelectedNodeInfo { Title = field.Name, Description = field.Description, IsCareerField = true, Careers = careers?.ToList() ?? new List() }; } } // Check if it's an event node else if (_nodeIdToEventId.TryGetValue(nodeId, out var eventId) && _allEvents != null) { var evt = _allEvents.FirstOrDefault(e => e.Id == eventId); if (evt != null) { _selectedNodeInfo = new SelectedNodeInfo { Title = evt.Name, Description = evt.Description, IsCareerField = false, Careers = evt.RelatedCareers.Select(c => c.Name).OrderBy(c => c).ToList() }; } } } StateHasChanged(); } }