680f61241a
This commit removes the Month and Day button group from the Calendar component, streamlining the user interface. The change enhances the overall layout and focuses on the MudCalendar display, improving user experience by reducing visual clutter. This update aligns with the ongoing efforts to refine component interactions and maintain a clean UI.
659 lines
27 KiB
Plaintext
659 lines
27 KiB
Plaintext
@page "/meeting-schedule"
|
|
@attribute [Authorize]
|
|
@using System.Text
|
|
@using Core.Calculation
|
|
@using Core.Models
|
|
@using Core.Utility
|
|
@using Microsoft.EntityFrameworkCore
|
|
@using WebApp.Components.Shared.Components
|
|
@using WebApp.Components.Features.MeetingSchedule
|
|
@using WebApp.Models
|
|
@using WebApp.Services
|
|
@inject IConfiguration Configuration
|
|
@inject ClipboardService ClipboardService
|
|
@inject IDialogService DialogService
|
|
@inject ISnackbar Snackbar
|
|
@inject IMeetingScheduleStateService StateService
|
|
@inject IMeetingScheduleDataService DataService
|
|
@inject IMeetingScheduleClipboardService ClipboardFormatService
|
|
@inject LocalStorageService LocalStorage
|
|
|
|
<PageHeader Title="@($"Meeting Scheduler Planner")">
|
|
<ActionButtons>
|
|
<MudTooltip Text="View meeting history">
|
|
<MudButton StartIcon="@Icons.Material.Filled.History"
|
|
Variant="Variant.Outlined"
|
|
Color="Color.Default"
|
|
Href="/meeting-schedule/history">
|
|
View History
|
|
</MudButton>
|
|
</MudTooltip>
|
|
<MudTooltip Text="Save current schedule as meeting history">
|
|
<MudButton StartIcon="@Icons.Material.Filled.Save"
|
|
Variant="Variant.Outlined"
|
|
Color="Color.Primary"
|
|
OnClick="OpenSaveHistoryDialog"
|
|
Disabled="@(!_scheduledTeams.Any())">
|
|
Save to History
|
|
</MudButton>
|
|
</MudTooltip>
|
|
<PageNoteButton PageIdentifier="Meeting Schedule" />
|
|
</ActionButtons>
|
|
</PageHeader>
|
|
|
|
<MudPaper Elevation="2" Class="pa-3 pa-md-6 mt-4">
|
|
<MudGrid>
|
|
<MudItem xs="12" sm="8" lg="9">
|
|
<MudText Typo="Typo.h4">Time Slots</MudText>
|
|
<MudPaper Class="pa-2 ma-2" Elevation="3">
|
|
<MudGrid>
|
|
<MudItem xs="12" sm="6" lg="4">
|
|
<MudPaper Elevation="0" Class="pa-1" Style="border: 1px solid var(--mud-palette-lines-default); border-radius: var(--mud-default-borderradius);">
|
|
<MudStack Row="true" AlignItems="AlignItems.Center" Spacing="2">
|
|
<MudText Typo="Typo.body2">Time Slots</MudText>
|
|
<MudStack Row="true" AlignItems="AlignItems.Center" Spacing="0" Style="border: 1px solid var(--mud-palette-lines-default); border-radius: var(--mud-default-borderradius);">
|
|
<MudIconButton Icon="@Icons.Material.Filled.Remove"
|
|
OnClick="DecrementTimeSlots"
|
|
Disabled="@(_parameters.TimeSlots <= 1)"
|
|
Size="Size.Small"
|
|
Variant="Variant.Text"
|
|
Style="border-radius: var(--mud-default-borderradius) 0 0 var(--mud-default-borderradius);" />
|
|
<MudButton Disabled="true"
|
|
Variant="Variant.Text"
|
|
Style="min-width: 50px; width: 50px; pointer-events: none; border-left: 1px solid var(--mud-palette-lines-default); border-right: 1px solid var(--mud-palette-lines-default); border-radius: 0;">
|
|
@_parameters.TimeSlots
|
|
</MudButton>
|
|
<MudIconButton Icon="@Icons.Material.Filled.Add"
|
|
OnClick="IncrementTimeSlots"
|
|
Disabled="@(_parameters.TimeSlots >= 4)"
|
|
Size="Size.Small"
|
|
Variant="Variant.Text"
|
|
Style="border-radius: 0 var(--mud-default-borderradius) var(--mud-default-borderradius) 0;" />
|
|
</MudStack>
|
|
</MudStack>
|
|
</MudPaper>
|
|
</MudItem>
|
|
<MudItem xs="12" sm="6" lg="4">
|
|
<AddRemoveFilter Label="High Effort"
|
|
OnAdd="AddHighLevelOfEffort"
|
|
OnRemove="RemoveHighLevelOfEffort"
|
|
AddTooltip="Schedule teams with Level of Effort >= 3"
|
|
RemoveTooltip="Remove teams with Level of Effort >= 3" />
|
|
</MudItem>
|
|
<MudItem xs="12" sm="6" lg="4">
|
|
<AddRemoveFilter Label="Regionals"
|
|
OnAdd="AddRegionals"
|
|
OnRemove="RemoveRegionals"
|
|
AddTooltip="Add regional event teams"
|
|
RemoveTooltip="Remove regional event teams" />
|
|
</MudItem>
|
|
<MudItem xs="12" sm="6" lg="4">
|
|
<AddRemoveFilter Label="Individual"
|
|
OnAdd="AddIndividual"
|
|
OnRemove="RemoveIndividual"
|
|
AddTooltip="Add individual event teams"
|
|
RemoveTooltip="Remove individual event teams" />
|
|
</MudItem>
|
|
<MudItem xs="12" sm="6" lg="4">
|
|
<AddRemoveFilter Label="Low Effort"
|
|
OnAdd="AddLowLevelOfEffort"
|
|
OnRemove="RemoveLowLevelOfEffort"
|
|
AddTooltip="Add teams with Level of Effort <= 1"
|
|
RemoveTooltip="Remove teams with Level of Effort <= 1" />
|
|
</MudItem>
|
|
<MudItem xs="12" sm="6" lg="4">
|
|
<MudButton Variant="Variant.Outlined" OnClick="Invert" FullWidth="true">Invert</MudButton>
|
|
</MudItem>
|
|
|
|
<MudItem xs="12" sm="6" lg="4">
|
|
<MudTooltip Text="Load teams from clipboard text by matching team names">
|
|
<MudButton Variant="Variant.Outlined" OnClick="LoadTeamsFromClipboard" FullWidth="true" Disabled="@_isLoadingClipboard">Load from Clipboard</MudButton>
|
|
</MudTooltip>
|
|
</MudItem>
|
|
<MudItem xs="12" sm="6" lg="4">
|
|
<MudButton Variant="Variant.Outlined" Color="Color.Warning" OnClick="Reset" FullWidth="true">Reset</MudButton>
|
|
</MudItem>
|
|
<MudItem xs="12">
|
|
<MudStack Row="true" AlignItems="AlignItems.Center" Spacing="2">
|
|
<MudSpacer />
|
|
<MudTooltip Text="Copy to Clipboard">
|
|
<MudIconButton OnClick="CopyToClipboard" Icon="@Icons.Material.Filled.ContentCopy"></MudIconButton>
|
|
</MudTooltip>
|
|
<MudButton Variant="@(IsDirty() ? Variant.Outlined : Variant.Filled)"
|
|
OnClick="Solve"
|
|
Color="Color.Primary"
|
|
Disabled="@_isSolving">
|
|
Solve
|
|
</MudButton>
|
|
</MudStack>
|
|
</MudItem>
|
|
</MudGrid>
|
|
</MudPaper>
|
|
|
|
<MudTable T="TeamScheduleTimeSlot" ServerData="SolveSchedule" @ref="_solutionData">
|
|
<HeaderContent>
|
|
</HeaderContent>
|
|
<RowTemplate>
|
|
<MudTd>
|
|
<MudGrid>
|
|
<MudItem xs="12" lg="6">
|
|
<ScheduledTeamsList TimeSlotName="@context.Name"
|
|
TimeSlotIndex="@GetTimeSlotIndex(context.Name)"
|
|
Teams="@context.Teams"
|
|
ScheduledTeams="@_scheduledTeams"
|
|
AbsentStudents="@_absentStudents"
|
|
StudentHasOverlaps="@context.StudentHasOverlaps"
|
|
ExcludedStudents="@_excludedStudents"
|
|
OnToggleTeam="@ToggleRequiredTeam"
|
|
OnToggleStudentExclusion="EventCallback.Factory.Create<(int teamId, int timeSlotIndex, int studentId)>(this, OnToggleStudentExclusion)" />
|
|
</MudItem>
|
|
<MudItem xs="12" lg="6">
|
|
<UnscheduledStudentsList UnscheduledStudents="@context.UnscheduledStudents"
|
|
AbsentStudents="@_absentStudents"
|
|
ScheduledTeams="@_scheduledTeams"
|
|
PossibleAdditions="@_possibleAdditions"
|
|
UnassignedTeams="@_solution.StudentUnassignedTeams"
|
|
OnToggleTeam="@ToggleRequiredTeam" />
|
|
</MudItem>
|
|
</MudGrid>
|
|
</MudTd>
|
|
</RowTemplate>
|
|
</MudTable>
|
|
</MudItem>
|
|
<MudItem xs="12" sm="4" lg="3">
|
|
<MudStack>
|
|
<StudentTextBoxSelector Students="@_students"
|
|
SelectedStudents="_absentStudents"
|
|
SelectedStudentsChanged="OnAbsentStudentsChanged"
|
|
Title="Absent Students"
|
|
Label="Search for absent students"
|
|
ShowFullName="true"/>
|
|
<MudDivider Class="my-4"/>
|
|
<TeamMeetingToggleSelector Teams="@_teams"
|
|
SelectedTeams="_scheduledTeams"
|
|
SelectedTeamsChanged="OnScheduledTeamsChanged"
|
|
ExtendedTeams="_extendedTeams"
|
|
ExtendedTeamsChanged="OnExtendedTeamsChanged"
|
|
Title="Scheduled Teams"
|
|
ShowEventAttributes="false" />
|
|
</MudStack>
|
|
</MudItem>
|
|
|
|
</MudGrid>
|
|
</MudPaper>
|
|
|
|
@code {
|
|
private Team[] _teams = null!;
|
|
private Student[] _students = null!;
|
|
MudTable<TeamScheduleTimeSlot> _solutionData = null!;
|
|
private TeamSchedulerSolution _solution = null!;
|
|
private TeamSchedulerOptions _parameters = null!;
|
|
bool _isSolving;
|
|
private bool _isLoadingClipboard = false;
|
|
private IEnumerable<Team> _scheduledTeams = [];
|
|
private IEnumerable<Student> _absentStudents = [];
|
|
private IEnumerable<Team> _possibleAdditions = [];
|
|
private IEnumerable<Team> _extendedTeams = [];
|
|
// Key: (teamId, timeSlotIndex, studentId) - Value: true if excluded
|
|
private Dictionary<(int teamId, int timeSlotIndex, int studentId), bool> _excludedStudents = new();
|
|
// Track last saved state for dirty/clean comparison
|
|
private MeetingScheduleState? _lastSavedState;
|
|
|
|
private void OnScheduledTeamsChanged(IEnumerable<Team> teams)
|
|
{
|
|
_scheduledTeams = teams;
|
|
StateHasChanged();
|
|
}
|
|
|
|
private void OnAbsentStudentsChanged(IEnumerable<Student> students)
|
|
{
|
|
_absentStudents = students;
|
|
StateHasChanged();
|
|
}
|
|
|
|
private async Task OnTimeSlotCountChanged(int timeSlots)
|
|
{
|
|
_parameters.TimeSlots = timeSlots;
|
|
StateHasChanged();
|
|
await Task.CompletedTask;
|
|
}
|
|
|
|
private async Task IncrementTimeSlots()
|
|
{
|
|
if (_parameters.TimeSlots < 4)
|
|
{
|
|
await OnTimeSlotCountChanged(_parameters.TimeSlots + 1);
|
|
}
|
|
}
|
|
|
|
private async Task DecrementTimeSlots()
|
|
{
|
|
if (_parameters.TimeSlots > 1)
|
|
{
|
|
await OnTimeSlotCountChanged(_parameters.TimeSlots - 1);
|
|
}
|
|
}
|
|
|
|
private void OnExtendedTeamsChanged(IEnumerable<Team> teams)
|
|
{
|
|
_extendedTeams = teams;
|
|
StateHasChanged();
|
|
}
|
|
|
|
private void AddRegionals()
|
|
{
|
|
_scheduledTeams = _scheduledTeams.AddRegionals(_teams);
|
|
StateHasChanged();
|
|
}
|
|
|
|
private void AddHighLevelOfEffort()
|
|
{
|
|
_scheduledTeams = _scheduledTeams.AddHighLevelOfEffort(_teams);
|
|
StateHasChanged();
|
|
}
|
|
|
|
private void RemoveHighLevelOfEffort()
|
|
{
|
|
var highEffortTeamIds = _teams.Where(t => t.Event.LevelOfEffort >= 3).Select(t => t.Id).ToHashSet();
|
|
_scheduledTeams = _scheduledTeams.Where(t => !highEffortTeamIds.Contains(t.Id));
|
|
StateHasChanged();
|
|
}
|
|
|
|
private void RemoveRegionals()
|
|
{
|
|
var regionalTeamIds = _teams.Where(t => t.Event.RegionalEvent).Select(t => t.Id).ToHashSet();
|
|
_scheduledTeams = _scheduledTeams.Where(t => !regionalTeamIds.Contains(t.Id));
|
|
StateHasChanged();
|
|
}
|
|
|
|
private void AddIndividual()
|
|
{
|
|
var individualTeams = _teams.Where(t => t.Event.EventFormat == EventFormat.Individual);
|
|
_scheduledTeams = _scheduledTeams.Concat(individualTeams).Distinct();
|
|
StateHasChanged();
|
|
}
|
|
|
|
private void RemoveIndividual()
|
|
{
|
|
_scheduledTeams = _scheduledTeams.RemoveIndividual();
|
|
StateHasChanged();
|
|
}
|
|
|
|
private void AddLowLevelOfEffort()
|
|
{
|
|
var lowEffortTeams = _teams.Where(t => t.Event.LevelOfEffort <= 1);
|
|
_scheduledTeams = _scheduledTeams.Concat(lowEffortTeams).Distinct();
|
|
StateHasChanged();
|
|
}
|
|
|
|
private void RemoveLowLevelOfEffort()
|
|
{
|
|
_scheduledTeams = _scheduledTeams.RemoveLowLevelOfEffort();
|
|
StateHasChanged();
|
|
}
|
|
|
|
private void Invert()
|
|
{
|
|
_scheduledTeams = _scheduledTeams.Invert(_teams);
|
|
StateHasChanged();
|
|
}
|
|
|
|
private void Reset()
|
|
{
|
|
_scheduledTeams = [];
|
|
_extendedTeams = [];
|
|
_excludedStudents.Clear();
|
|
StateHasChanged();
|
|
}
|
|
|
|
private void ToggleRequiredTeam(Team unassignedTeam)
|
|
{
|
|
// Find the matching team from _teams to ensure reference equality with MudToggleGroup
|
|
var matchingTeam = _teams.FirstOrDefault(t => t.Id == unassignedTeam.Id);
|
|
if (matchingTeam == null) return;
|
|
|
|
var scheduledTeamIds = _scheduledTeams.Select(t => t.Id).ToHashSet();
|
|
IEnumerable<Team> newScheduledTeams;
|
|
|
|
if (scheduledTeamIds.Contains(matchingTeam.Id))
|
|
newScheduledTeams = _scheduledTeams.Where(t => t.Id != matchingTeam.Id);
|
|
else
|
|
{
|
|
newScheduledTeams = _scheduledTeams.Concat([matchingTeam]);
|
|
}
|
|
|
|
// Update state and notify component to re-render
|
|
OnScheduledTeamsChanged(newScheduledTeams);
|
|
}
|
|
|
|
protected override async Task OnInitializedAsync()
|
|
{
|
|
_parameters =
|
|
new TeamSchedulerOptions(
|
|
2,
|
|
mustIncludeEvents:
|
|
[
|
|
// "Medical Technology", "Electrical Applications" , "RegionalTeam",
|
|
// ,"Dragster", "Flight"
|
|
],
|
|
extended:
|
|
[
|
|
// "Invention", "Construction Challenge", "Mechanical", "Mass", "Micro"
|
|
//"STEM"
|
|
//"Community", "Vlogging"// "Microcontroller"
|
|
],
|
|
omittedEvents:
|
|
[
|
|
// "Vlogging", "Junior", "Community Service Video", "Digital Photography",
|
|
// "STEM"
|
|
|
|
//"Leadership",// "Electrical", //"Construction"
|
|
// "Forensic",
|
|
//"CAD"
|
|
//"I&I Team 1", "I&I Team 2"//, "Website Design",
|
|
],
|
|
absentStudents:
|
|
[
|
|
]
|
|
);
|
|
|
|
_teams = await DataService.LoadTeamsAsync();
|
|
_students = await DataService.LoadStudentsAsync();
|
|
|
|
// Load saved selections from localStorage
|
|
_scheduledTeams = await StateService.LoadScheduledTeamsAsync(_teams);
|
|
_absentStudents = await StateService.LoadAbsentStudentsAsync(_students);
|
|
_parameters.TimeSlots = await StateService.LoadTimeSlotCountAsync(2);
|
|
_extendedTeams = await StateService.LoadExtendedTeamsAsync(_teams);
|
|
_excludedStudents = await StateService.LoadExcludedStudentsAsync();
|
|
|
|
// Initialize last saved state from loaded values
|
|
_lastSavedState = await MeetingScheduleState.FromLocalStorage(LocalStorage, _teams, _students);
|
|
if (_lastSavedState == null)
|
|
{
|
|
// If no saved state exists, create initial state from current values
|
|
_lastSavedState = MeetingScheduleState.FromCurrent(
|
|
_scheduledTeams,
|
|
_absentStudents,
|
|
_parameters.TimeSlots,
|
|
_extendedTeams,
|
|
_excludedStudents);
|
|
}
|
|
}
|
|
|
|
|
|
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 void OnToggleStudentExclusion((int teamId, int timeSlotIndex, int studentId) key)
|
|
{
|
|
if (_excludedStudents.TryGetValue(key, out var isExcluded) && isExcluded)
|
|
{
|
|
_excludedStudents.Remove(key);
|
|
}
|
|
else
|
|
{
|
|
_excludedStudents[key] = true;
|
|
}
|
|
StateHasChanged();
|
|
}
|
|
|
|
private bool IsStudentExcluded(int teamId, int timeSlotIndex, int studentId)
|
|
{
|
|
var key = (teamId, timeSlotIndex, studentId);
|
|
return _excludedStudents.TryGetValue(key, out var isExcluded) && isExcluded;
|
|
}
|
|
|
|
private Team[] GetTeamsWithoutExcludedStudents(Team[] teams, int timeSlotIndex)
|
|
{
|
|
var absentStudentIds = _absentStudents.Select(s => s.Id).ToHashSet();
|
|
return OverlapCalculationHelper.GetTeamsWithoutExcludedStudents(teams, timeSlotIndex, _excludedStudents, absentStudentIds);
|
|
}
|
|
|
|
private bool IsDirty()
|
|
{
|
|
if (_lastSavedState == null)
|
|
return true;
|
|
|
|
var currentState = MeetingScheduleState.FromCurrent(
|
|
_scheduledTeams,
|
|
_absentStudents,
|
|
_parameters.TimeSlots,
|
|
_extendedTeams,
|
|
_excludedStudents);
|
|
|
|
return !currentState.Equals(_lastSavedState);
|
|
}
|
|
|
|
private async Task<TableData<TeamScheduleTimeSlot>> SolveSchedule(TableState arg1, CancellationToken arg2)
|
|
{
|
|
_isSolving = true;
|
|
|
|
// Check if there are any teams to schedule
|
|
if (!_scheduledTeams.Any())
|
|
{
|
|
_isSolving = false;
|
|
_solution = new TeamSchedulerSolution([], [], "No teams selected");
|
|
return new TableData<TeamScheduleTimeSlot> { Items = [] };
|
|
}
|
|
|
|
// Filter out absent students
|
|
var availableStudents = _students.Where(s => !_absentStudents.Contains(s)).ToArray();
|
|
|
|
// Check if there are any available students
|
|
if (availableStudents.Length == 0)
|
|
{
|
|
_isSolving = false;
|
|
_solution = new TeamSchedulerSolution([], [], "No available students");
|
|
return new TableData<TeamScheduleTimeSlot> { Items = [] };
|
|
}
|
|
|
|
// Update parameters with absent student names
|
|
_parameters.AbsentStudents = _absentStudents.Select(s => s.FirstNameLastName).ToArray();
|
|
|
|
// Update parameters with extended team event names
|
|
_parameters.ExtendedTeams = _extendedTeams
|
|
.Where(t => t.Event != null)
|
|
.Select(t => t.Event!.Name)
|
|
.ToArray();
|
|
|
|
// Create PartialTeam instances for teams with excluded students
|
|
var teamsForScheduling = PartialTeam.CreatePartialTeamsFromExclusions(_scheduledTeams, _excludedStudents);
|
|
|
|
var teamScheduler = new TeamScheduler(teamsForScheduling, _parameters.TimeSlots, availableStudents);
|
|
_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 and unscheduled students
|
|
var teamsForOverlapCalculation = GetTeamsWithoutExcludedStudents(slot.Teams, slotIndex);
|
|
slot.StudentOverlaps = TeamSchedulerSolution.GetStudentTeamOverlaps(teamsForOverlapCalculation);
|
|
// Use teams without excluded students so students excluded from all teams appear as unscheduled
|
|
slot.UnscheduledStudents = TeamSchedulerSolution.GetStudentsNotInTimSlot(teamsForOverlapCalculation, availableStudents);
|
|
}
|
|
|
|
// Post-process: extend teams to next consecutive time slot
|
|
if (_extendedTeams.Any())
|
|
{
|
|
TeamSchedulerPostProcessor.ExtendTeamsInSolution(
|
|
_solution,
|
|
_extendedTeams,
|
|
availableStudents,
|
|
GetTeamsWithoutExcludedStudents);
|
|
}
|
|
|
|
// Try recommendation strategies in priority order
|
|
var scheduler = new UnassignedStudentScheduler(_teams, _solution.TimeSlots);
|
|
UnassignedScheduleStrategy[] strategies =
|
|
[
|
|
UnassignedScheduleStrategy.LevelOfEffort,
|
|
UnassignedScheduleStrategy.BiggestGroup,
|
|
UnassignedScheduleStrategy.AnyNotMeetingAlready,
|
|
UnassignedScheduleStrategy.IndividualEvents
|
|
];
|
|
|
|
_possibleAdditions = strategies
|
|
.Select(strategy => scheduler.ScheduleStrategy(strategy))
|
|
.FirstOrDefault(result => result.Any()) ?? [];
|
|
|
|
// Save state to localStorage after solving completes successfully
|
|
var currentState = MeetingScheduleState.FromCurrent(
|
|
_scheduledTeams,
|
|
_absentStudents,
|
|
_parameters.TimeSlots,
|
|
_extendedTeams,
|
|
_excludedStudents);
|
|
await currentState.SaveToLocalStorage(LocalStorage);
|
|
_lastSavedState = currentState;
|
|
|
|
// Also save via state service for consistency
|
|
await StateService.SaveScheduledTeamsAsync(_scheduledTeams);
|
|
await StateService.SaveAbsentStudentsAsync(_absentStudents);
|
|
await StateService.SaveTimeSlotCountAsync(_parameters.TimeSlots);
|
|
await StateService.SaveExtendedTeamsAsync(_extendedTeams);
|
|
await StateService.SaveExcludedStudentsAsync(_excludedStudents);
|
|
|
|
await InvokeAsync(StateHasChanged); // let the UI know that the solution has been found
|
|
|
|
_isSolving = false;
|
|
return new TableData<TeamScheduleTimeSlot> { Items = _solution.TimeSlots };
|
|
}
|
|
|
|
private void Solve()
|
|
{
|
|
_solutionData.ReloadServerData();
|
|
}
|
|
|
|
|
|
async Task CopyToClipboard()
|
|
{
|
|
try
|
|
{
|
|
var text = ClipboardFormatService.FormatScheduleForClipboard(
|
|
_solution,
|
|
_teams,
|
|
_absentStudents,
|
|
_excludedStudents,
|
|
GetTimeSlotIndex);
|
|
await ClipboardService.WriteTextAsync(text);
|
|
}
|
|
catch
|
|
{
|
|
Console.WriteLine("Cannot write text to clipboard");
|
|
}
|
|
}
|
|
|
|
private async Task OpenSaveHistoryDialog()
|
|
{
|
|
var parameters = new DialogParameters
|
|
{
|
|
["ScheduledTeams"] = _scheduledTeams,
|
|
["AbsentStudents"] = _absentStudents,
|
|
["AllTeams"] = _teams,
|
|
["AllStudents"] = _students
|
|
};
|
|
|
|
var options = new DialogOptions
|
|
{
|
|
MaxWidth = MaxWidth.Medium,
|
|
FullWidth = true,
|
|
CloseButton = true
|
|
};
|
|
|
|
var dialog = await DialogService.ShowAsync<SaveMeetingHistoryDialog>("Save Meeting History", parameters, options);
|
|
var result = await dialog.Result;
|
|
|
|
// Note: Success message is already shown in the dialog, no need to show another here
|
|
}
|
|
|
|
private async Task LoadTeamsFromClipboard()
|
|
{
|
|
if (_isLoadingClipboard) return;
|
|
|
|
try
|
|
{
|
|
_isLoadingClipboard = true;
|
|
StateHasChanged();
|
|
|
|
var clipboardText = await ClipboardService.ReadTextAsync();
|
|
|
|
if (string.IsNullOrWhiteSpace(clipboardText))
|
|
{
|
|
Snackbar.Add("Clipboard is empty", Severity.Warning);
|
|
return;
|
|
}
|
|
|
|
var matchedTeams = TeamClipboardMatcher.MatchTeamsFromClipboard(clipboardText, _teams);
|
|
|
|
if (!matchedTeams.Any())
|
|
{
|
|
Snackbar.Add("No matching teams found in clipboard text", Severity.Info);
|
|
return;
|
|
}
|
|
|
|
// Combine with existing scheduled teams, avoiding duplicates
|
|
var existingTeamIds = _scheduledTeams.Select(t => t.Id).ToHashSet();
|
|
var newTeams = matchedTeams.Where(t => !existingTeamIds.Contains(t.Id));
|
|
var updatedTeams = _scheduledTeams.Concat(newTeams).ToList();
|
|
|
|
OnScheduledTeamsChanged(updatedTeams);
|
|
|
|
var newCount = newTeams.Count();
|
|
var totalCount = matchedTeams.Count();
|
|
if (newCount == totalCount)
|
|
{
|
|
Snackbar.Add($"Selected {totalCount} team(s) from clipboard", Severity.Success);
|
|
}
|
|
else
|
|
{
|
|
Snackbar.Add($"Selected {newCount} new team(s) from clipboard ({totalCount - newCount} already selected)", Severity.Success);
|
|
}
|
|
}
|
|
catch (JSException ex)
|
|
{
|
|
Snackbar.Add("Unable to access clipboard. Please ensure clipboard permissions are granted.", Severity.Error);
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
Snackbar.Add($"Error loading teams from clipboard: {ex.Message}", Severity.Error);
|
|
}
|
|
finally
|
|
{
|
|
_isLoadingClipboard = false;
|
|
StateHasChanged();
|
|
}
|
|
}
|
|
|
|
|
|
} |