8af86e22d9
This commit updates various files across the Core and WebApp projects to replace traditional collection initializers with C# 12 collection expressions. Changes include modifications to EventAssignment.cs, TeamScheduler_DecisionTree.cs, CareerField.cs, EventDefinition.cs, and several components in the WebApp. These updates enhance code readability and maintainability by adhering to modern C# syntax standards.
311 lines
12 KiB
Plaintext
311 lines
12 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-3 pa-md-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-3 pa-md-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-3 pa-md-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>
|
|
|
|
<div style="width: 100%; height: 600px; border: 1px solid #ddd; border-radius: 4px;">
|
|
<Network Id="careerMappingNetwork" Data="@_networkData" Options="@GetNetworkOptions" OnClick="HandleNetworkClick" />
|
|
</div>
|
|
|
|
@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>
|
|
<div class="career-mapping">
|
|
<MudStack Row="true" Spacing="1" Wrap="Wrap.Wrap">
|
|
@foreach (var career in _selectedNodeInfo.Careers.OrderBy(c => c))
|
|
{
|
|
<MudChip T="string" Size="Size.Small" Variant="Variant.Filled" Color="Color.Default">@career</MudChip>
|
|
}
|
|
</MudStack>
|
|
</div>
|
|
}
|
|
</MudPaper>
|
|
}
|
|
|
|
</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] = [];
|
|
}
|
|
// 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)
|
|
{
|
|
List<Node> nodes = [];
|
|
List<Edge> edges = [];
|
|
|
|
// 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() ?? []
|
|
};
|
|
}
|
|
}
|
|
// 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();
|
|
}
|
|
|
|
} |