[feat:] im- and export (pdf/json)

This commit is contained in:
2026-06-04 18:59:01 +02:00
parent 5ef41b21b0
commit 66596b530b
5 changed files with 242 additions and 51 deletions
+57 -12
View File
@@ -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);
}
}
+10 -2
View File
@@ -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()
+61 -8
View File
@@ -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<Student> 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<string>()
};
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<string> interestedStudents)
+49 -1
View File
@@ -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<Sport> 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);
}
}
private sealed class ResultExportEntry
{
public int Semester { get; set; }
public string SportName { get; set; } = string.Empty;
public List<string> 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);
}
}
+65 -28
View File
@@ -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<Student> ImportResultFromFile(string path)
{
var dict = new Dictionary<string, (string Name, List<string> Courses)>();
var dict = new Dictionary<string, string[]>(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<string>());
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<Student>();
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<string>())
{
Result = entry.Value
})
.ToList();
}
}
private sealed class ResultImportEntry
{
public int Semester { get; set; }
public string SportName { get; set; } = string.Empty;
public List<string> 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<List<ResultImportEntry>>(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();
}
}
}