Files
chapter-organizer/WebApp/Components/Features/Events/CareerMapping.razor
T
poprhythm 3bd076afb3 Enhance CareerMapping component with node click functionality and detailed career display
Updated the CareerMapping component to allow users to click on nodes for detailed information about career fields and related careers. Introduced a new SelectedNodeInfo class to manage the display of selected node details. Improved data handling for career field and event nodes, ensuring accurate representation of related careers. Adjusted the network click event to trigger updates in the UI, enhancing interactivity and user experience.
2025-12-29 21:19:08 -05:00

301 lines
11 KiB
Plaintext

@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
<PageHeader
Title="Career Mapping"
Subtitle="Event-Career Relationships"
ShowBackButton="true"
BackButtonUrl="/events"/>
@if (_isLoading)
{
<MudPaper Elevation="2" Class="pa-6">
<MudProgressLinear Indeterminate="true" Color="Color.Primary"/>
<MudText Typo="Typo.body1" Class="mt-4">Loading career mapping data...</MudText>
</MudPaper>
}
else if (_networkData == null || !_networkData.Nodes.Any())
{
<MudPaper Elevation="2" Class="pa-6">
<MudText Typo="Typo.h6" Class="mb-4">No Career Field Mappings Found</MudText>
<MudText Typo="Typo.body1" Class="mud-text-secondary">
No events have related careers assigned that match any career fields. Edit events to add related careers.
</MudText>
</MudPaper>
}
else
{
<MudPaper Elevation="2" Class="pa-6">
<MudText Typo="Typo.h6" Class="mb-4">Event-Career Field Relationships</MudText>
<MudText Typo="Typo.body2" Class="mb-4 mud-text-secondary">
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.
</MudText>
@if (_selectedNodeInfo != null)
{
<MudPaper Elevation="1" Class="pa-4 mb-4" Style="background-color: #f5f5f5;">
<MudText Typo="Typo.h6" Class="mb-2">@_selectedNodeInfo.Title</MudText>
@if (_selectedNodeInfo.Careers != null && _selectedNodeInfo.Careers.Any())
{
<MudText Typo="Typo.subtitle2" Class="mb-2">Related Careers:</MudText>
<MudStack Row="true" Spacing="1" WrapItems="true">
@foreach (var career in _selectedNodeInfo.Careers.OrderBy(c => c))
{
<MudChip T="string" Size="Size.Small" Variant="Variant.Filled" Color="Color.Default">@career</MudChip>
}
</MudStack>
}
</MudPaper>
}
<div style="width: 100%; height: 600px; border: 1px solid #ddd; border-radius: 4px;">
<Network Id="careerMappingNetwork" Data="@_networkData" Options="@GetNetworkOptions" OnClick="HandleNetworkClick"/>
</div>
</MudPaper>
}
@code {
private NetworkData? _networkData;
private bool _isLoading = true;
private Dictionary<int, List<string>> _fieldIdToCareers = new();
private List<EventDefinition>? _allEvents;
private Dictionary<string, int> _nodeIdToFieldId = new();
private Dictionary<string, int> _nodeIdToEventId = new();
private SelectedNodeInfo? _selectedNodeInfo;
private class SelectedNodeInfo
{
public string Title { get; set; } = string.Empty;
public bool IsCareerField { get; set; }
public List<string>? 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<string>();
}
// 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<EventDefinition> events)
{
var nodes = new List<Node>();
var edges = new List<Edge>();
// Dictionary to track node IDs (to avoid duplicates)
var eventNodeIds = new Dictionary<int, string>();
var fieldNodeIds = new Dictionary<int, string>();
var eventCounter = 1;
var fieldCounter = 1;
// Dictionary to track which events connect to which career fields
var eventToFields = new Dictionary<int, HashSet<CareerField>>();
// 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<CareerField>(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,
IsCareerField = true,
Careers = careers?.ToList() ?? new List<string>()
};
}
}
// 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,
IsCareerField = false,
Careers = evt.RelatedCareers.Select(c => c.Name).OrderBy(c => c).ToList()
};
}
}
}
StateHasChanged();
}
}