c6fb00c7f4
Added a Description property to the CareerField class to provide a short overview of each career field. Updated the CareerFieldDefinitions to include descriptions for all career fields. Modified the CareerMapping component to display the description of the selected career field or event, improving user experience by providing more context about each node.
308 lines
11 KiB
Plaintext
308 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 (!string.IsNullOrWhiteSpace(_selectedNodeInfo.Description))
|
|
{
|
|
<MudText Typo="Typo.body2" Class="mb-3 mud-text-secondary">@_selectedNodeInfo.Description</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 string? Description { get; set; }
|
|
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,
|
|
Description = field.Description,
|
|
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,
|
|
Description = evt.Description,
|
|
IsCareerField = false,
|
|
Careers = evt.RelatedCareers.Select(c => c.Name).OrderBy(c => c).ToList()
|
|
};
|
|
}
|
|
}
|
|
}
|
|
|
|
StateHasChanged();
|
|
}
|
|
|
|
} |