using System; using System.Collections.Generic; using System.Diagnostics; using System.Linq; using System.Threading.Tasks; using Avalonia.Controls; using Avalonia.Input; using Avalonia.Interactivity; using Avalonia.Platform.Storage; namespace spplus; public partial class MainWindow : Window { public static MainWindow Instance { get; set; } public static string ApplicationVersion = "v1.2.24"; private sealed class ResultEntry { public Student Student { get; } public int Semester { get; } public string CourseName { get; } public ResultEntry(Student student, int semester, string? courseName) { Student = student; Semester = semester; CourseName = courseName ?? string.Empty; } public override string ToString() { return $"{Student.Name} ({Student.ID}) - {Semester}. Semester: {CourseName}"; } } public MainWindow() { InitializeComponent(); Settings.ImportInitial(); Instance = this; RefreshCoursesList(); try { NudSportMaxPerSemester1.Value = Settings.Instance.NumCoursesPerSemester[0]; NudSportMaxPerSemester2.Value = Settings.Instance.NumCoursesPerSemester[1]; NudSportMaxPerSemester3.Value = Settings.Instance.NumCoursesPerSemester[2]; NudSportMaxPerSemester4.Value = Settings.Instance.NumCoursesPerSemester[3]; } catch {} } private void RegenerateContextMenu() { MnuChange.Items.Clear(); foreach (var sport in Settings.Instance.Sports) { var item = new MenuItem { Header = sport.Name }; item.Click += (_, _) => ChangeStudentCourse(sport); MnuChange.Items.Add(item); } } private async void ChangeStudentCourse(Sport targetSport) { if (LbResult.SelectedItem is not ResultEntry selectedEntry) return; try { if (await ApplyStudentCourseChange(selectedEntry.Student, selectedEntry.Semester, targetSport)) { CourseCrafter.ReloadResult(); RefreshResultView(); RegenerateContextMenu(); } } catch (Exception ex) { Console.WriteLine(ex.Message + "\n" + ex.StackTrace); } } private async Task ApplyStudentCourseChange(Student student, int semester, Sport targetSport) { if (semester < 1 || semester > 4) return false; var semesterCourses = CourseCrafter.GeneratedCourses .Where(course => course.Semester == semester) .ToList(); var currentCourses = semesterCourses .Where(course => course.Instance.Students.Contains(student.ID)) .ToList(); string? oldSportName = currentCourses .Select(course => course.Instance.Sport.Name) .FirstOrDefault(); var existingTargetCourses = semesterCourses .Where(course => course.Instance.Sport.Name == targetSport.Name) .ToList(); CourseCrafter.CourseInstance? targetCourse = existingTargetCourses .Where(course => !course.Instance.Students.Contains(student.ID) && course.Instance.Students.Count < course.Instance.Sport.MaxStudents) .OrderBy(course => course.Instance.Students.Count) .Select(course => course.Instance) .FirstOrDefault(); if (targetCourse == null && existingTargetCourses.Any()) { targetCourse = existingTargetCourses .OrderBy(course => course.Instance.Students.Count) .Select(course => course.Instance) .FirstOrDefault(); } bool changed = false; foreach (var course in currentCourses) { if (targetCourse != null && ReferenceEquals(course.Instance, targetCourse)) continue; if (course.Instance.Students.Remove(student.ID)) changed = true; } int removedEmptyCourses = CourseCrafter.GeneratedCourses.RemoveAll(course => course.Semester == semester && course.Instance.Students.Count == 0 && !ReferenceEquals(course.Instance, targetCourse)); if (removedEmptyCourses > 0) changed = true; if (targetCourse == null) { var newCourse = new CourseCrafter.CourseInstance { Sport = targetSport, Students = new List { student.ID } }; CourseCrafter.GeneratedCourses.Add((semester, newCourse)); changed = true; } else if (!targetCourse.Students.Contains(student.ID)) { targetCourse.Students.Add(student.ID); changed = true; } if (changed && !string.IsNullOrEmpty(oldSportName) && IsRebalanceNeededForSportSemester(oldSportName, semester)) { var result = await MessageBox.Show(this, $"Der Kursplan für {oldSportName} im {semester}. Semester ist nach der Änderung unausgeglichen. Soll die alte Sportart umverteilt werden?", "Umverteilung der alten Sportart", MessageBoxButton.YesNo); if (result == MessageBoxResult.Yes) BalanceSportSemester(oldSportName, semester); } return changed; } private bool IsRebalanceNeededForSportSemester(string sportName, int semester) { var courses = CourseCrafter.GeneratedCourses .Where(course => course.Semester == semester && course.Instance.Sport.Name == sportName) .ToList(); if (courses.Count <= 1) return false; var ordered = courses .OrderBy(course => course.Instance.Students.Count) .ToList(); int minCount = ordered.First().Instance.Students.Count; int maxCount = ordered.Last().Instance.Students.Count; if (maxCount - minCount <= 1) return false; if (ordered.Last().Instance.Students.Count - 1 < GetEffectiveMinStudents(ordered.Last().Instance.Sport, semester)) return false; if (ordered.First().Instance.Students.Count >= ordered.First().Instance.Sport.MaxStudents) return false; return true; } private void BalanceSportSemester(string sportName, int semester) { bool changed; do { changed = false; var courses = CourseCrafter.GeneratedCourses .Where(course => course.Semester == semester && course.Instance.Sport.Name == sportName) .OrderBy(course => course.Instance.Students.Count) .ToList(); if (courses.Count <= 1) break; var target = courses.First(); var source = courses.Last(); if (source.Instance.Students.Count <= target.Instance.Students.Count + 1) break; if (source.Instance.Students.Count - 1 < GetEffectiveMinStudents(source.Instance.Sport, semester)) break; if (target.Instance.Students.Count >= target.Instance.Sport.MaxStudents) break; var studentId = source.Instance.Students[^1]; source.Instance.Students.RemoveAt(source.Instance.Students.Count - 1); target.Instance.Students.Add(studentId); changed = true; } while (changed); } private int GetEffectiveMinStudents(Sport sport, int semester) { int reduction = (semester >= 3) ? 2 : 0; return Math.Max(1, sport.MinStudents - reduction); } private async void MnuExpSettings_OnClick(object? sender, RoutedEventArgs e) { await ExportConfigurationAsync(); } private void MnuExit_OnClick(object? sender, RoutedEventArgs e) { Environment.Exit(0); } private void MnuHelp_OnClick(object? sender, RoutedEventArgs e) { try { Process.Start(new ProcessStartInfo { FileName = "https://git.mypapercloud.de/fierke/spplus/wiki", UseShellExecute = true }); } catch (Exception ex) { Console.WriteLine($"Fehler beim Öffnen des Links: {ex.Message}"); } } private void MnuGit_OnClick(object? sender, RoutedEventArgs e) { try { Process.Start(new ProcessStartInfo { FileName = "https://git.mypapercloud.de/fierke/spplus", UseShellExecute = true }); } catch (Exception ex) { Console.WriteLine($"Fehler beim Öffnen des Links: {ex.Message}"); } } private void MnuAbout_OnClick(object? sender, RoutedEventArgs e) { Window w = new(); w.WindowState = WindowState.Normal; w.WindowStartupLocation = WindowStartupLocation.CenterScreen; w.Width = 300; w.Height = 120; Grid g = new(); TextBlock tb = new() { Text = $"spplus {MainWindow.ApplicationVersion}\n(c)2026 MyPapertown, Elias Fierke" }; g.Children.Add(tb); w.Content = g; w.Show(); } private async void BtnImport_OnClick(object? sender, RoutedEventArgs e) { var topLevel = GetTopLevel(this); var file = await topLevel!.StorageProvider.OpenFilePickerAsync(new FilePickerOpenOptions { Title = "CSV-Datei auswählen", AllowMultiple = false, FileTypeFilter = new[] { new FilePickerFileType(".csv-Datei") { Patterns = new[] { "*.csv" } } } }); if (file == null) return; if (file.Count == 0) return; var imported_students = import.ImportStudentsFromFile(file[0].Path.LocalPath.ToString()); foreach (var s in imported_students) { Settings.Instance.Students.Add(s); } RefreshImportedStudentList(); } private void RefreshImportedStudentList() { LbStudentsImported.Items.Clear(); int count_selected = 0; foreach (var s in Settings.Instance.Students) { LbStudentsImported.Items.Add(s); count_selected += s.SelectedCourseNames.Count; } LblStudentAmount.Content = Settings.Instance.Students.Count.ToString(); LblSelectedAmount.Content = count_selected.ToString(); List<(Sport, List)> initial_sportlist = new(); foreach (var sp in Settings.Instance.Sports) { initial_sportlist.Add((sp, new())); } foreach (Student s in Settings.Instance.Students) { foreach (var sp in s.SelectedCourseNames) { foreach (var item in initial_sportlist) { if (item.Item1.AlternativeNames.Contains(sp)) { item.Item2.Add(s.ID); break; } } } } LblNumVoted.Content = ""; foreach (var s in initial_sportlist) { LblNumVoted.Content += $"{s.Item1.Name}: {s.Item2.Count}\n"; } } private void BtnCraftCourses_OnClick(object? sender, RoutedEventArgs e) { CourseCrafter.Craft(); RefreshResultView(); TclMainView.SelectedIndex = 2; //TbiResults.Focus(); } private void LbResult_OnPointerPressed(object? sender, PointerPressedEventArgs e) { if (!e.GetCurrentPoint(LbResult).Properties.IsRightButtonPressed) return; if (e.Source is Control control && control.DataContext is ResultEntry entry) { LbResult.SelectedItem = entry; } else { LbResult.SelectedItem = null; } } private void RefreshResultView() { LbResult.Items.Clear(); foreach (Student s in Settings.Instance.Students) { try { for(int i = 0; i 0) { CourseCrafter.GeneratedCourses = imported .OrderBy(c => c.Semester) .ThenBy(c => c.Instance.Sport.Name) .ToList(); CourseCrafter.ReloadResult(); RefreshResultView(); } } private async System.Threading.Tasks.Task ExportConfigurationAsync() { var topLevel = GetTopLevel(this); var file = await topLevel!.StorageProvider.SaveFilePickerAsync(new FilePickerSaveOptions { Title = "Konfiguration speichern", SuggestedFileName = "spplus_konfiguration.json", SuggestedFileType = new FilePickerFileType(".json-Datei") { Patterns = new[] { "*.json" } } }); if (file == null) return; ExportUtility.ExportConfigurationToJson(file.Path.LocalPath); } }