Enhance MeetingSchedule component to support student exclusion management
This commit introduces functionality in the MeetingSchedule feature to manage student exclusions during scheduling. The Index.razor component has been updated to include methods for saving and loading excluded students, as well as UI elements for toggling exclusions. Additionally, the TeamScheduler logic has been modified to account for excluded students when calculating overlaps and scheduling teams. These changes improve the flexibility and accuracy of the scheduling process, enhancing the overall user experience.
This commit is contained in:
@@ -82,10 +82,11 @@ public class TeamScheduler
|
|||||||
var model = new CpModel();
|
var model = new CpModel();
|
||||||
|
|
||||||
// Build membership matrix: m[i,t] = 1 if student i is on team t, else 0
|
// Build membership matrix: m[i,t] = 1 if student i is on team t, else 0
|
||||||
|
// Use team.Students to support PartialTeam objects with omitted students
|
||||||
var m = new int[_students.Length,_teams.Length];
|
var m = new int[_students.Length,_teams.Length];
|
||||||
foreach (var i in _students)
|
foreach (var i in _students)
|
||||||
foreach (var t in _teams)
|
foreach (var t in _teams)
|
||||||
m[i, t] = _studentObjects[i].Teams.Any(team => team.Id == _teamObjects[t].Id) ? 1 : 0;
|
m[i, t] = _teamObjects[t].Students.Any(s => s.Id == _studentObjects[i].Id) ? 1 : 0;
|
||||||
|
|
||||||
// Decision variables:
|
// Decision variables:
|
||||||
// x[t,s] = 1 if meeting of team t takes place at time slot s, else 0
|
// x[t,s] = 1 if meeting of team t takes place at time slot s, else 0
|
||||||
|
|||||||
@@ -63,11 +63,14 @@
|
|||||||
<MudGrid>
|
<MudGrid>
|
||||||
<MudItem xs="12" lg="6">
|
<MudItem xs="12" lg="6">
|
||||||
<ScheduledTeamsList TimeSlotName="@context.Name"
|
<ScheduledTeamsList TimeSlotName="@context.Name"
|
||||||
|
TimeSlotIndex="@GetTimeSlotIndex(context.Name)"
|
||||||
Teams="@context.Teams"
|
Teams="@context.Teams"
|
||||||
ScheduledTeams="@_scheduledTeams"
|
ScheduledTeams="@_scheduledTeams"
|
||||||
AbsentStudents="@_absentStudents"
|
AbsentStudents="@_absentStudents"
|
||||||
StudentHasOverlaps="@context.StudentHasOverlaps"
|
StudentHasOverlaps="@context.StudentHasOverlaps"
|
||||||
OnToggleTeam="@ToggleRequiredTeam" />
|
ExcludedStudents="@_excludedStudents"
|
||||||
|
OnToggleTeam="@ToggleRequiredTeam"
|
||||||
|
OnToggleStudentExclusion="EventCallback.Factory.Create<(int teamId, int timeSlotIndex, int studentId)>(this, OnToggleStudentExclusion)" />
|
||||||
</MudItem>
|
</MudItem>
|
||||||
<MudItem xs="12" lg="6">
|
<MudItem xs="12" lg="6">
|
||||||
<UnscheduledStudentsList UnscheduledStudents="@context.UnscheduledStudents"
|
<UnscheduledStudentsList UnscheduledStudents="@context.UnscheduledStudents"
|
||||||
@@ -115,6 +118,8 @@
|
|||||||
private IEnumerable<Student> _absentStudents = [];
|
private IEnumerable<Student> _absentStudents = [];
|
||||||
private IEnumerable<Team> _possibleAdditions = [];
|
private IEnumerable<Team> _possibleAdditions = [];
|
||||||
private IEnumerable<Team> _extendedTeams = [];
|
private IEnumerable<Team> _extendedTeams = [];
|
||||||
|
// Key: (teamId, timeSlotIndex, studentId) - Value: true if excluded
|
||||||
|
private Dictionary<(int teamId, int timeSlotIndex, int studentId), bool> _excludedStudents = new();
|
||||||
|
|
||||||
private async Task OnScheduledTeamsChanged(IEnumerable<Team> teams)
|
private async Task OnScheduledTeamsChanged(IEnumerable<Team> teams)
|
||||||
{
|
{
|
||||||
@@ -180,20 +185,26 @@
|
|||||||
{
|
{
|
||||||
_scheduledTeams = [];
|
_scheduledTeams = [];
|
||||||
_extendedTeams = [];
|
_extendedTeams = [];
|
||||||
|
_excludedStudents.Clear();
|
||||||
await SaveScheduledTeams();
|
await SaveScheduledTeams();
|
||||||
await SaveExtendedTeams();
|
await SaveExtendedTeams();
|
||||||
|
await SaveExcludedStudents();
|
||||||
}
|
}
|
||||||
|
|
||||||
private async void ToggleRequiredTeam(Team unassignedTeam)
|
private async Task ToggleRequiredTeam(Team unassignedTeam)
|
||||||
{
|
{
|
||||||
var scheduledTeamIds = _scheduledTeams.Select(t => t.Id).ToHashSet();
|
var scheduledTeamIds = _scheduledTeams.Select(t => t.Id).ToHashSet();
|
||||||
|
IEnumerable<Team> newScheduledTeams;
|
||||||
|
|
||||||
if (scheduledTeamIds.Contains(unassignedTeam.Id))
|
if (scheduledTeamIds.Contains(unassignedTeam.Id))
|
||||||
_scheduledTeams = _scheduledTeams.Where(t => t.Id != unassignedTeam.Id);
|
newScheduledTeams = _scheduledTeams.Where(t => t.Id != unassignedTeam.Id);
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
_scheduledTeams = _scheduledTeams.Concat([unassignedTeam]);
|
newScheduledTeams = _scheduledTeams.Concat([unassignedTeam]);
|
||||||
}
|
}
|
||||||
await SaveScheduledTeams();
|
|
||||||
|
// Update state and notify component to re-render
|
||||||
|
await OnScheduledTeamsChanged(newScheduledTeams);
|
||||||
}
|
}
|
||||||
|
|
||||||
protected override async Task OnInitializedAsync()
|
protected override async Task OnInitializedAsync()
|
||||||
@@ -252,6 +263,7 @@
|
|||||||
await LoadAbsentStudents();
|
await LoadAbsentStudents();
|
||||||
await LoadTimeSlotCount();
|
await LoadTimeSlotCount();
|
||||||
await LoadExtendedTeams();
|
await LoadExtendedTeams();
|
||||||
|
await LoadExcludedStudents();
|
||||||
}
|
}
|
||||||
|
|
||||||
private async Task SaveScheduledTeams()
|
private async Task SaveScheduledTeams()
|
||||||
@@ -313,6 +325,90 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private async Task SaveExcludedStudents()
|
||||||
|
{
|
||||||
|
var exclusions = _excludedStudents.Keys
|
||||||
|
.Where(k => _excludedStudents[k])
|
||||||
|
.Select(k => new ExcludedStudent(k.teamId, k.timeSlotIndex, k.studentId))
|
||||||
|
.ToArray();
|
||||||
|
await LocalStorage.SetJsonAsync("MeetingSchedule_ExcludedStudents", exclusions);
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task LoadExcludedStudents()
|
||||||
|
{
|
||||||
|
var exclusions = await LocalStorage.GetJsonAsync<ExcludedStudent[]>("MeetingSchedule_ExcludedStudents");
|
||||||
|
if (exclusions != null && exclusions.Length > 0)
|
||||||
|
{
|
||||||
|
_excludedStudents = exclusions.ToDictionary(
|
||||||
|
e => (e.TeamId, e.TimeSlotIndex, e.StudentId),
|
||||||
|
_ => true);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private int GetTimeSlotIndex(string timeSlotName)
|
||||||
|
{
|
||||||
|
if (_solution?.TimeSlots == null)
|
||||||
|
return 0; // Default to first slot if solution not available
|
||||||
|
|
||||||
|
for (int i = 0; i < _solution.TimeSlots.Length; i++)
|
||||||
|
{
|
||||||
|
if (_solution.TimeSlots[i].Name == timeSlotName)
|
||||||
|
return i;
|
||||||
|
}
|
||||||
|
return 0; // Default to first slot if not found (shouldn't happen in normal flow)
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task OnToggleStudentExclusion((int teamId, int timeSlotIndex, int studentId) key)
|
||||||
|
{
|
||||||
|
if (_excludedStudents.TryGetValue(key, out var isExcluded) && isExcluded)
|
||||||
|
{
|
||||||
|
_excludedStudents.Remove(key);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
_excludedStudents[key] = true;
|
||||||
|
}
|
||||||
|
await SaveExcludedStudents();
|
||||||
|
StateHasChanged();
|
||||||
|
}
|
||||||
|
|
||||||
|
private bool IsStudentExcluded(int teamId, int timeSlotIndex, int studentId)
|
||||||
|
{
|
||||||
|
var key = (teamId, timeSlotIndex, studentId);
|
||||||
|
return _excludedStudents.TryGetValue(key, out var isExcluded) && isExcluded;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Creates teams with excluded students filtered out for overlap calculation.
|
||||||
|
/// </summary>
|
||||||
|
private Team[] GetTeamsWithoutExcludedStudents(Team[] teams, int timeSlotIndex)
|
||||||
|
{
|
||||||
|
return teams.Select(team =>
|
||||||
|
{
|
||||||
|
// Find excluded students for this team in this time slot
|
||||||
|
// More efficient: iterate through team students and check exclusions
|
||||||
|
var includedStudents = team.Students
|
||||||
|
.Where(s => !IsStudentExcluded(team.Id, timeSlotIndex, s.Id))
|
||||||
|
.ToList();
|
||||||
|
|
||||||
|
// If no students are excluded, return original team
|
||||||
|
if (includedStudents.Count == team.Students.Count)
|
||||||
|
return team;
|
||||||
|
|
||||||
|
// Create a temporary team with excluded students removed
|
||||||
|
return new Team
|
||||||
|
{
|
||||||
|
Id = team.Id,
|
||||||
|
Event = team.Event,
|
||||||
|
Students = includedStudents,
|
||||||
|
Captain = team.Captain,
|
||||||
|
Identifier = team.Identifier
|
||||||
|
};
|
||||||
|
}).ToArray();
|
||||||
|
}
|
||||||
|
|
||||||
|
private record ExcludedStudent(int TeamId, int TimeSlotIndex, int StudentId);
|
||||||
|
|
||||||
private async Task<TableData<TeamScheduleTimeSlot>> SolveSchedule(TableState arg1, CancellationToken arg2)
|
private async Task<TableData<TeamScheduleTimeSlot>> SolveSchedule(TableState arg1, CancellationToken arg2)
|
||||||
{
|
{
|
||||||
_isSolving = true;
|
_isSolving = true;
|
||||||
@@ -345,9 +441,60 @@
|
|||||||
.Select(t => t.Event!.Name)
|
.Select(t => t.Event!.Name)
|
||||||
.ToArray();
|
.ToArray();
|
||||||
|
|
||||||
var teamScheduler = new TeamScheduler(_scheduledTeams, _parameters.TimeSlots, availableStudents);
|
// Create PartialTeam instances for teams with excluded students
|
||||||
|
// Aggregate exclusions across all time slots (since we don't know assignment yet)
|
||||||
|
var teamsForScheduling = _scheduledTeams.Select(team =>
|
||||||
|
{
|
||||||
|
// Find all students excluded for this team across all time slots
|
||||||
|
var excludedStudentIds = _excludedStudents.Keys
|
||||||
|
.Where(k => k.teamId == team.Id && _excludedStudents[k])
|
||||||
|
.Select(k => k.studentId)
|
||||||
|
.Distinct()
|
||||||
|
.ToHashSet();
|
||||||
|
|
||||||
|
if (excludedStudentIds.Count == 0)
|
||||||
|
return team;
|
||||||
|
|
||||||
|
var excludedStudents = team.Students.Where(s => excludedStudentIds.Contains(s.Id)).ToList();
|
||||||
|
if (excludedStudents.Count == 0)
|
||||||
|
return team;
|
||||||
|
|
||||||
|
// Create PartialTeam with excluded students
|
||||||
|
return team.CloneWithOmittedStudents(excludedStudents);
|
||||||
|
}).ToArray();
|
||||||
|
|
||||||
|
var teamScheduler = new TeamScheduler(teamsForScheduling, _parameters.TimeSlots, availableStudents);
|
||||||
_solution = teamScheduler.Solve();
|
_solution = teamScheduler.Solve();
|
||||||
|
|
||||||
|
// Restore full teams (with all students) in the solution so excluded students still appear (dimmed)
|
||||||
|
// Create a mapping from PartialTeam to original Team
|
||||||
|
var teamMapping = _scheduledTeams.ToDictionary(t => t.Id, t => t);
|
||||||
|
|
||||||
|
for (int slotIndex = 0; slotIndex < _solution.TimeSlots.Length; slotIndex++)
|
||||||
|
{
|
||||||
|
var slot = _solution.TimeSlots[slotIndex];
|
||||||
|
if (slot.Teams == null)
|
||||||
|
continue;
|
||||||
|
|
||||||
|
var restoredTeams = slot.Teams.Select(team =>
|
||||||
|
{
|
||||||
|
// If this is a PartialTeam or we have the original, restore it
|
||||||
|
if (teamMapping.TryGetValue(team.Id, out var originalTeam))
|
||||||
|
{
|
||||||
|
return originalTeam;
|
||||||
|
}
|
||||||
|
return team;
|
||||||
|
}).ToArray();
|
||||||
|
|
||||||
|
slot.Teams = restoredTeams;
|
||||||
|
|
||||||
|
// Recalculate overlaps and unscheduled students with full teams
|
||||||
|
// Filter out excluded students when calculating overlaps
|
||||||
|
var teamsForOverlapCalculation = GetTeamsWithoutExcludedStudents(slot.Teams, slotIndex);
|
||||||
|
slot.StudentOverlaps = TeamSchedulerSolution.GetStudentTeamOverlaps(teamsForOverlapCalculation);
|
||||||
|
slot.UnscheduledStudents = TeamSchedulerSolution.GetStudentsNotInTimSlot(slot.Teams, availableStudents);
|
||||||
|
}
|
||||||
|
|
||||||
// Post-process: extend teams to next consecutive time slot
|
// Post-process: extend teams to next consecutive time slot
|
||||||
if (_extendedTeams.Any())
|
if (_extendedTeams.Any())
|
||||||
{
|
{
|
||||||
@@ -439,7 +586,9 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
previousSlot.Teams = previousSlotTeamsList.ToArray();
|
previousSlot.Teams = previousSlotTeamsList.ToArray();
|
||||||
previousSlot.StudentOverlaps = TeamSchedulerSolution.GetStudentTeamOverlaps(previousSlot.Teams);
|
var previousSlotIndex = slotIndex - 1;
|
||||||
|
var previousSlotTeamsForOverlap = GetTeamsWithoutExcludedStudents(previousSlot.Teams, previousSlotIndex);
|
||||||
|
previousSlot.StudentOverlaps = TeamSchedulerSolution.GetStudentTeamOverlaps(previousSlotTeamsForOverlap);
|
||||||
previousSlot.UnscheduledStudents = TeamSchedulerSolution.GetStudentsNotInTimSlot(previousSlot.Teams, allStudents);
|
previousSlot.UnscheduledStudents = TeamSchedulerSolution.GetStudentsNotInTimSlot(previousSlot.Teams, allStudents);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -467,6 +616,7 @@
|
|||||||
|
|
||||||
private void AppendScheduledTeams(StringBuilder sb, TeamScheduleTimeSlot timeslot)
|
private void AppendScheduledTeams(StringBuilder sb, TeamScheduleTimeSlot timeslot)
|
||||||
{
|
{
|
||||||
|
var timeSlotIndex = GetTimeSlotIndex(timeslot.Name);
|
||||||
foreach (var scheduledTeam in timeslot.Teams.OrderBy(e => e.ToString()))
|
foreach (var scheduledTeam in timeslot.Teams.OrderBy(e => e.ToString()))
|
||||||
{
|
{
|
||||||
var teamName = scheduledTeam.ToString();
|
var teamName = scheduledTeam.ToString();
|
||||||
@@ -477,17 +627,37 @@
|
|||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
var studentsList = FormatStudentList(scheduledTeam, timeslot);
|
var studentsList = FormatStudentList(scheduledTeam, timeslot, timeSlotIndex);
|
||||||
sb.Append($"{teamName} - {studentsList}");
|
sb.Append($"{teamName} - {studentsList}");
|
||||||
}
|
}
|
||||||
sb.Append(Environment.NewLine);
|
sb.Append(Environment.NewLine);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private string FormatStudentList(Team team, TeamScheduleTimeSlot timeslot)
|
private string FormatStudentList(Team team, TeamScheduleTimeSlot timeslot, int timeSlotIndex)
|
||||||
{
|
{
|
||||||
|
// Filter out excluded students for this team and time slot
|
||||||
|
var excludedStudentIds = _excludedStudents.Keys
|
||||||
|
.Where(k => k.teamId == team.Id && k.timeSlotIndex == timeSlotIndex && _excludedStudents[k])
|
||||||
|
.Select(k => k.studentId)
|
||||||
|
.ToHashSet();
|
||||||
|
|
||||||
|
var includedStudents = team.Students
|
||||||
|
.Where(s => !excludedStudentIds.Contains(s.Id))
|
||||||
|
.ToList();
|
||||||
|
|
||||||
|
// Create a temporary team with only included students for formatting
|
||||||
|
var teamForFormatting = new Team
|
||||||
|
{
|
||||||
|
Id = team.Id,
|
||||||
|
Event = team.Event,
|
||||||
|
Students = includedStudents,
|
||||||
|
Captain = team.Captain,
|
||||||
|
Identifier = team.Identifier
|
||||||
|
};
|
||||||
|
|
||||||
return TeamStudentNameFormatter.FormatStudentList(
|
return TeamStudentNameFormatter.FormatStudentList(
|
||||||
team,
|
teamForFormatting,
|
||||||
new TeamStudentNameFormatter.FormatOptions
|
new TeamStudentNameFormatter.FormatOptions
|
||||||
{
|
{
|
||||||
Ordering = TeamStudentNameFormatter.OrderingStyle.CaptainFirst,
|
Ordering = TeamStudentNameFormatter.OrderingStyle.CaptainFirst,
|
||||||
|
|||||||
@@ -36,10 +36,14 @@
|
|||||||
</MudTooltip>
|
</MudTooltip>
|
||||||
</MudStack>
|
</MudStack>
|
||||||
<MudStack Row="true" Spacing="1" Wrap="Wrap.Wrap" AlignItems="AlignItems.Center">
|
<MudStack Row="true" Spacing="1" Wrap="Wrap.Wrap" AlignItems="AlignItems.Center">
|
||||||
|
@{
|
||||||
|
var nonExcludedStudentCount = GetNonExcludedStudentCount(team);
|
||||||
|
}
|
||||||
@foreach (var student in team.Students)
|
@foreach (var student in team.Students)
|
||||||
{
|
{
|
||||||
var overlap = StudentHasOverlaps(student);
|
var overlap = StudentHasOverlaps(student);
|
||||||
var chipColor = overlap ? Color.Warning : Color.Default;
|
var chipColor = overlap ? Color.Warning : Color.Default;
|
||||||
|
var isExcluded = IsStudentExcluded(team.Id, TimeSlotIndex, student.Id);
|
||||||
var formattedName = TeamStudentNameFormatter.FormatStudentName(
|
var formattedName = TeamStudentNameFormatter.FormatStudentName(
|
||||||
student,
|
student,
|
||||||
team,
|
team,
|
||||||
@@ -51,9 +55,29 @@
|
|||||||
AbsentStudents = AbsentStudents.ToList()
|
AbsentStudents = AbsentStudents.ToList()
|
||||||
});
|
});
|
||||||
|
|
||||||
<MudChip T="string" Size="Size.Small" Color="@chipColor" Variant="Variant.Outlined">
|
<MudStack Row="true" Spacing="0" AlignItems="AlignItems.Center"
|
||||||
@formattedName
|
Style="position: relative; display: inline-flex;"
|
||||||
|
@onmouseenter="@(() => _hoveredStudent = (team.Id, student.Id))"
|
||||||
|
@onmouseleave="@(() => _hoveredStudent = (null, null))">
|
||||||
|
<MudChip T="string"
|
||||||
|
Size="Size.Small"
|
||||||
|
Color="@chipColor"
|
||||||
|
Variant="Variant.Outlined"
|
||||||
|
Style="@(isExcluded ? "opacity: 0.5;" : "")">
|
||||||
|
<MudStack Row="true" Spacing="1" AlignItems="AlignItems.Center">
|
||||||
|
<span>@formattedName</span>
|
||||||
|
@if (_hoveredStudent == (team.Id, student.Id) && nonExcludedStudentCount > 1)
|
||||||
|
{
|
||||||
|
<MudIconButton Icon="@Icons.Material.Filled.Close"
|
||||||
|
Size="Size.Small"
|
||||||
|
Color="Color.Error"
|
||||||
|
Variant="Variant.Text"
|
||||||
|
OnClick="@(() => OnToggleStudentExclusion.InvokeAsync((team.Id, TimeSlotIndex, student.Id)))"
|
||||||
|
Style="padding: 0; min-width: 16px; width: 16px; height: 16px; margin-left: 4px;" />
|
||||||
|
}
|
||||||
|
</MudStack>
|
||||||
</MudChip>
|
</MudChip>
|
||||||
|
</MudStack>
|
||||||
}
|
}
|
||||||
</MudStack>
|
</MudStack>
|
||||||
</MudStack>
|
</MudStack>
|
||||||
@@ -64,6 +88,9 @@
|
|||||||
[Parameter]
|
[Parameter]
|
||||||
public string TimeSlotName { get; set; } = string.Empty;
|
public string TimeSlotName { get; set; } = string.Empty;
|
||||||
|
|
||||||
|
[Parameter]
|
||||||
|
public int TimeSlotIndex { get; set; }
|
||||||
|
|
||||||
[Parameter]
|
[Parameter]
|
||||||
public IEnumerable<Team> Teams { get; set; } = [];
|
public IEnumerable<Team> Teams { get; set; } = [];
|
||||||
|
|
||||||
@@ -76,6 +103,25 @@
|
|||||||
[Parameter]
|
[Parameter]
|
||||||
public Func<Student, bool> StudentHasOverlaps { get; set; } = null!;
|
public Func<Student, bool> StudentHasOverlaps { get; set; } = null!;
|
||||||
|
|
||||||
|
[Parameter]
|
||||||
|
public Dictionary<(int teamId, int timeSlotIndex, int studentId), bool> ExcludedStudents { get; set; } = new();
|
||||||
|
|
||||||
[Parameter]
|
[Parameter]
|
||||||
public EventCallback<Team> OnToggleTeam { get; set; }
|
public EventCallback<Team> OnToggleTeam { get; set; }
|
||||||
|
|
||||||
|
[Parameter]
|
||||||
|
public EventCallback<(int teamId, int timeSlotIndex, int studentId)> OnToggleStudentExclusion { get; set; }
|
||||||
|
|
||||||
|
private (int? teamId, int? studentId) _hoveredStudent = (null, null);
|
||||||
|
|
||||||
|
private bool IsStudentExcluded(int teamId, int timeSlotIndex, int studentId)
|
||||||
|
{
|
||||||
|
var key = (teamId, timeSlotIndex, studentId);
|
||||||
|
return ExcludedStudents.ContainsKey(key) && ExcludedStudents[key];
|
||||||
|
}
|
||||||
|
|
||||||
|
private int GetNonExcludedStudentCount(Team team)
|
||||||
|
{
|
||||||
|
return team.Students.Count(s => !IsStudentExcluded(team.Id, TimeSlotIndex, s.Id));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -147,6 +147,49 @@ public sealed class LocalStorageService
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets a JSON-serialized object from localStorage.
|
||||||
|
/// </summary>
|
||||||
|
/// <typeparam name="T">The type to deserialize to.</typeparam>
|
||||||
|
/// <param name="key">The storage key.</param>
|
||||||
|
/// <returns>The deserialized object or default value if not found.</returns>
|
||||||
|
public async Task<T?> GetJsonAsync<T>(string key)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var json = await _jsRuntime.InvokeAsync<string?>("localStorage.getItem", key);
|
||||||
|
if (!string.IsNullOrEmpty(json))
|
||||||
|
{
|
||||||
|
return JsonSerializer.Deserialize<T>(json);
|
||||||
|
}
|
||||||
|
return default;
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
_logger.LogWarning(ex, "Failed to load JSON from localStorage [{Key}]", key);
|
||||||
|
return default;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Sets a JSON-serialized object in localStorage.
|
||||||
|
/// </summary>
|
||||||
|
/// <typeparam name="T">The type to serialize.</typeparam>
|
||||||
|
/// <param name="key">The storage key.</param>
|
||||||
|
/// <param name="value">The object to store.</param>
|
||||||
|
public async Task SetJsonAsync<T>(string key, T value)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var json = JsonSerializer.Serialize(value);
|
||||||
|
await _jsRuntime.InvokeVoidAsync("localStorage.setItem", key, json);
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
_logger.LogWarning(ex, "Failed to save JSON to localStorage [{Key}]", key);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Clears all items from localStorage.
|
/// Clears all items from localStorage.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
|
|||||||
Reference in New Issue
Block a user