diff --git a/MainWindow.axaml.cs b/MainWindow.axaml.cs index 5cccbbe..189ed1e 100644 --- a/MainWindow.axaml.cs +++ b/MainWindow.axaml.cs @@ -143,9 +143,9 @@ public partial class MainWindow : Window return changed; } - private void MnuExpSettings_OnClick(object? sender, RoutedEventArgs e) + private async void MnuExpSettings_OnClick(object? sender, RoutedEventArgs e) { - var res = MessageBox.Show(this, "Dieses Feature ist noch nicht implementiert", "Fehlend"); + await ExportConfigurationAsync(); } private void MnuExit_OnClick(object? sender, RoutedEventArgs e) @@ -547,9 +547,22 @@ public partial class MainWindow : Window } catch (Exception ex){} } - private void BtnExportCoursePDF_OnClick(object? sender, RoutedEventArgs e) + private async void BtnExportCoursePDF_OnClick(object? sender, RoutedEventArgs e) { - // Export as PDF + var topLevel = GetTopLevel(this); + var file = await topLevel!.StorageProvider.SaveFilePickerAsync(new FilePickerSaveOptions + { + Title = "PDF-Datei speichern", + SuggestedFileName = "spplus_kurse.pdf", + SuggestedFileType = new FilePickerFileType(".pdf-Datei") + { + Patterns = new[] { "*.pdf" } + } + }); + + if (file == null) return; + + PdfExportUtility.ExportGeneratedCourses(file.Path.LocalPath); } private void NudSportMaxPerSemester1_OnValueChanged(object? sender, NumericUpDownValueChangedEventArgs e) @@ -590,33 +603,65 @@ public partial class MainWindow : Window var file = await topLevel!.StorageProvider.SaveFilePickerAsync(new FilePickerSaveOptions { Title = "CSV-Datei speichern", - SuggestedFileType = new FilePickerFileType(".csv-Datei") + SuggestedFileName = "spplus_ergebnisse.json", + SuggestedFileType = new FilePickerFileType(".json-Datei") { - Patterns = new[] { "*.csv" } + Patterns = new[] { "*.json" } } }); if (file == null) return; - ExportUtility.ExportToCSV(file.Path.AbsolutePath); + ExportUtility.ExportResultsToJson(file.Path.LocalPath); } private async void MnuImpResult_OnClick(object? sender, RoutedEventArgs e) { - // Hier importieren var topLevel = GetTopLevel(this); var file = await topLevel!.StorageProvider.OpenFilePickerAsync(new FilePickerOpenOptions { - Title = "CSV-Datei laden", - SuggestedFileType = new FilePickerFileType(".csv-Datei") + Title = "Ergebnis-CSV laden", + AllowMultiple = false, + FileTypeFilter = new[] { - Patterns = new[] { "*.csv" } + new FilePickerFileType(".json-Datei") + { + Patterns = new[] { "*.json" } + } + } + }); + + if (file == null || file.Count == 0) return; + + var imported = import.ImportResultFromJson(file[0].Path.LocalPath.ToString()); + if (imported != null && imported.Count > 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); } } diff --git a/Program.cs b/Program.cs index c4121ad..20bbe28 100644 --- a/Program.cs +++ b/Program.cs @@ -1,5 +1,7 @@ using Avalonia; using System; +using PdfSharp; +using PdfSharp.Fonts; namespace spplus; @@ -9,8 +11,14 @@ class Program // SynchronizationContext-reliant code before AppMain is called: things aren't initialized // yet and stuff might break. [STAThread] - public static void Main(string[] args) => BuildAvaloniaApp() - .StartWithClassicDesktopLifetime(args); + public static void Main(string[] args) + { + // Initialize PdfSharp font resolver before any PDF operations + GlobalFontSettings.FontResolver = new CustomFontResolver(); + + BuildAvaloniaApp() + .StartWithClassicDesktopLifetime(args); + } // Avalonia configuration, don't remove; also used by visual designer. public static AppBuilder BuildAvaloniaApp() diff --git a/crafter.cs b/crafter.cs index a68838e..d6f7ff2 100644 --- a/crafter.cs +++ b/crafter.cs @@ -15,6 +15,66 @@ public class CourseCrafter public static List<(int Semester, CourseInstance Instance)> GeneratedCourses = new(); + private static Sport? ResolveSportFromCourseName(string courseName) + { + if (string.IsNullOrWhiteSpace(courseName) || + string.Equals(courseName, "null", StringComparison.OrdinalIgnoreCase)) + { + return null; + } + + return Settings.Instance.Sports.FirstOrDefault(sport => + string.Equals(sport.Name, courseName, StringComparison.OrdinalIgnoreCase) || + sport.AlternativeNames.Any(alt => string.Equals(alt, courseName, StringComparison.OrdinalIgnoreCase))); + } + + public static void RebuildGeneratedCoursesFromResults(IEnumerable importedResults) + { + var importedById = importedResults + .Where(student => !string.IsNullOrWhiteSpace(student.ID)) + .GroupBy(student => student.ID, StringComparer.OrdinalIgnoreCase) + .ToDictionary(group => group.Key, group => group.First(), StringComparer.OrdinalIgnoreCase); + + var generatedCourses = new Dictionary<(int Semester, string SportName), CourseInstance>(); + + foreach (var student in Settings.Instance.Students) + { + if (!importedById.TryGetValue(student.ID, out var importedStudent)) + continue; + + for (int semesterIndex = 0; semesterIndex < 4; semesterIndex++) + { + if (importedStudent.Result.Length <= semesterIndex) + continue; + + var resultName = importedStudent.Result[semesterIndex]; + var sport = ResolveSportFromCourseName(resultName); + if (sport == null) + continue; + + var key = (semesterIndex + 1, sport.Name); + if (!generatedCourses.TryGetValue(key, out var course)) + { + course = new CourseInstance + { + Sport = sport, + Students = new List() + }; + generatedCourses[key] = course; + } + + if (!course.Students.Contains(student.ID)) + course.Students.Add(student.ID); + } + } + + GeneratedCourses = generatedCourses + .Select(entry => (Semester: entry.Key.Semester, Instance: entry.Value)) + .OrderBy(course => course.Semester) + .ThenBy(course => course.Instance.Sport.Name) + .ToList(); + } + public static void Craft() { GeneratedCourses = new(); @@ -651,14 +711,7 @@ public class CourseCrafter Sport? ResolveSportFromSelection(string selectedCourseName) { - if (string.IsNullOrWhiteSpace(selectedCourseName) || - string.Equals(selectedCourseName, "null", StringComparison.OrdinalIgnoreCase)) - { - return null; - } - - return Settings.Instance.Sports - .FirstOrDefault(sport => sport.AlternativeNames.Contains(selectedCourseName)); + return ResolveSportFromCourseName(selectedCourseName); } int getSemesterForSport(Sport sp, List interestedStudents) diff --git a/exporter.cs b/exporter.cs index 6f03c50..396c228 100644 --- a/exporter.cs +++ b/exporter.cs @@ -1,9 +1,19 @@ +using System; +using System.Collections.Generic; +using System.Linq; using System.IO; +using System.Text.Json; namespace spplus; public static class ExportUtility { + private sealed class ConfigurationExport + { + public List Sports { get; set; } = []; + public int[] NumCoursesPerSemester { get; set; } = []; + } + public static void ExportToCSV(string filepath) { char separator = ','; @@ -16,4 +26,42 @@ public static class ExportUtility File.WriteAllText(filepath, output); } -} \ No newline at end of file + + private sealed class ResultExportEntry + { + public int Semester { get; set; } + public string SportName { get; set; } = string.Empty; + public List Students { get; set; } = new(); + } + + public static void ExportResultsToJson(string filepath) + { + var list = CourseCrafter.GeneratedCourses + .Select(g => new ResultExportEntry + { + Semester = g.Semester, + SportName = g.Instance.Sport.Name, + Students = g.Instance.Students.ToList() + }) + .ToList(); + + var json = JsonSerializer.Serialize(list, new JsonSerializerOptions { WriteIndented = true }); + File.WriteAllText(filepath, json); + } + + public static void ExportConfigurationToJson(string filepath) + { + var export = new ConfigurationExport + { + Sports = Settings.Instance.Sports, + NumCoursesPerSemester = Settings.Instance.NumCoursesPerSemester + }; + + var json = JsonSerializer.Serialize(export, new JsonSerializerOptions + { + WriteIndented = true + }); + + File.WriteAllText(filepath, json); + } +} diff --git a/import.cs b/import.cs index 148d62c..7d6eb66 100644 --- a/import.cs +++ b/import.cs @@ -1,6 +1,7 @@ using System; using System.Collections.Generic; using System.IO; +using System.Text.Json; using System.Linq; namespace spplus; @@ -50,42 +51,78 @@ public static class import public static List ImportResultFromFile(string path) { - var dict = new Dictionary Courses)>(); + var dict = new Dictionary(StringComparer.OrdinalIgnoreCase); foreach (var line in File.ReadLines(path).Skip(1)) // Header überspringen { if (string.IsNullOrWhiteSpace(line)) continue; - var parts = line.Split(','); - if (parts.Length < 3) + var parts = line.Split(',', StringSplitOptions.None); + if (parts.Length < 5) continue; - string nameWithId = parts[0].Trim(); - string course = parts[2].Replace("(2)", "").Replace("(3)", "").Replace("(4)", "").Trim(); - - int open = nameWithId.LastIndexOf('('); - int close = nameWithId.LastIndexOf(')'); - if (open < 0 || close < 0 || close <= open) - continue; - - string name = nameWithId[..open].Trim(); - string id = nameWithId[(open + 1)..close].Trim(); - - if (!dict.ContainsKey(id)) - dict[id] = (name, new List()); - - dict[id].Courses.Add(course); + var studentId = parts[0].Trim(); + dict[studentId] = new[] + { + parts[1].Trim(), + parts[2].Trim(), + parts[3].Trim(), + parts[4].Trim() + }; } - var result = new List(); - - foreach (var (id, data) in dict) - { - var student = new Student(id, data.Name, data.Courses); - result.Add(student); - } - - return result; + return dict + .Select(entry => new Student(entry.Key, string.Empty, new List()) + { + Result = entry.Value + }) + .ToList(); } -} \ No newline at end of file + + private sealed class ResultImportEntry + { + public int Semester { get; set; } + public string SportName { get; set; } = string.Empty; + public List Students { get; set; } = new(); + } + + public static List<(int Semester, CourseCrafter.CourseInstance Instance)> ImportResultFromJson(string path) + { + try + { + var json = File.ReadAllText(path); + var entries = JsonSerializer.Deserialize>(json); + if (entries == null) return new(); + + var result = new List<(int Semester, CourseCrafter.CourseInstance Instance)>(); + + foreach (var e in entries) + { + if (string.IsNullOrWhiteSpace(e.SportName)) + continue; + + var sport = Settings.Instance.Sports.FirstOrDefault(s => + string.Equals(s.Name, e.SportName, StringComparison.OrdinalIgnoreCase) || + s.AlternativeNames.Any(a => string.Equals(a, e.SportName, StringComparison.OrdinalIgnoreCase))); + + if (sport == null) + continue; + + var ci = new CourseCrafter.CourseInstance + { + Sport = sport, + Students = e.Students.Distinct().ToList() + }; + + result.Add((e.Semester, ci)); + } + + return result; + } + catch + { + return new(); + } + } +}