[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; 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) private void MnuExit_OnClick(object? sender, RoutedEventArgs e)
@@ -547,9 +547,22 @@ public partial class MainWindow : Window
} catch (Exception ex){} } 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) 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 var file = await topLevel!.StorageProvider.SaveFilePickerAsync(new FilePickerSaveOptions
{ {
Title = "CSV-Datei speichern", 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; if (file == null) return;
ExportUtility.ExportToCSV(file.Path.AbsolutePath); ExportUtility.ExportResultsToJson(file.Path.LocalPath);
} }
private async void MnuImpResult_OnClick(object? sender, RoutedEventArgs e) private async void MnuImpResult_OnClick(object? sender, RoutedEventArgs e)
{ {
// Hier importieren
var topLevel = GetTopLevel(this); var topLevel = GetTopLevel(this);
var file = await topLevel!.StorageProvider.OpenFilePickerAsync(new FilePickerOpenOptions var file = await topLevel!.StorageProvider.OpenFilePickerAsync(new FilePickerOpenOptions
{ {
Title = "CSV-Datei laden", Title = "Ergebnis-CSV laden",
SuggestedFileType = new FilePickerFileType(".csv-Datei") 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; if (file == null) return;
ExportUtility.ExportConfigurationToJson(file.Path.LocalPath);
} }
} }
+10 -2
View File
@@ -1,5 +1,7 @@
using Avalonia; using Avalonia;
using System; using System;
using PdfSharp;
using PdfSharp.Fonts;
namespace spplus; namespace spplus;
@@ -9,8 +11,14 @@ class Program
// SynchronizationContext-reliant code before AppMain is called: things aren't initialized // SynchronizationContext-reliant code before AppMain is called: things aren't initialized
// yet and stuff might break. // yet and stuff might break.
[STAThread] [STAThread]
public static void Main(string[] args) => BuildAvaloniaApp() public static void Main(string[] args)
.StartWithClassicDesktopLifetime(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. // Avalonia configuration, don't remove; also used by visual designer.
public static AppBuilder BuildAvaloniaApp() public static AppBuilder BuildAvaloniaApp()
+61 -8
View File
@@ -15,6 +15,66 @@ public class CourseCrafter
public static List<(int Semester, CourseInstance Instance)> GeneratedCourses public static List<(int Semester, CourseInstance Instance)> GeneratedCourses
= new(); = 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() public static void Craft()
{ {
GeneratedCourses = new(); GeneratedCourses = new();
@@ -651,14 +711,7 @@ public class CourseCrafter
Sport? ResolveSportFromSelection(string selectedCourseName) Sport? ResolveSportFromSelection(string selectedCourseName)
{ {
if (string.IsNullOrWhiteSpace(selectedCourseName) || return ResolveSportFromCourseName(selectedCourseName);
string.Equals(selectedCourseName, "null", StringComparison.OrdinalIgnoreCase))
{
return null;
}
return Settings.Instance.Sports
.FirstOrDefault(sport => sport.AlternativeNames.Contains(selectedCourseName));
} }
int getSemesterForSport(Sport sp, List<string> interestedStudents) 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.IO;
using System.Text.Json;
namespace spplus; namespace spplus;
public static class ExportUtility 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) public static void ExportToCSV(string filepath)
{ {
char separator = ','; char separator = ',';
@@ -16,4 +26,42 @@ public static class ExportUtility
File.WriteAllText(filepath, output); 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;
using System.Collections.Generic; using System.Collections.Generic;
using System.IO; using System.IO;
using System.Text.Json;
using System.Linq; using System.Linq;
namespace spplus; namespace spplus;
@@ -50,42 +51,78 @@ public static class import
public static List<Student> ImportResultFromFile(string path) 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 foreach (var line in File.ReadLines(path).Skip(1)) // Header überspringen
{ {
if (string.IsNullOrWhiteSpace(line)) if (string.IsNullOrWhiteSpace(line))
continue; continue;
var parts = line.Split(','); var parts = line.Split(',', StringSplitOptions.None);
if (parts.Length < 3) if (parts.Length < 5)
continue; continue;
string nameWithId = parts[0].Trim(); var studentId = parts[0].Trim();
string course = parts[2].Replace("(2)", "").Replace("(3)", "").Replace("(4)", "").Trim(); dict[studentId] = new[]
{
int open = nameWithId.LastIndexOf('('); parts[1].Trim(),
int close = nameWithId.LastIndexOf(')'); parts[2].Trim(),
if (open < 0 || close < 0 || close <= open) parts[3].Trim(),
continue; parts[4].Trim()
};
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 result = new List<Student>(); return dict
.Select(entry => new Student(entry.Key, string.Empty, new List<string>())
foreach (var (id, data) in dict) {
{ Result = entry.Value
var student = new Student(id, data.Name, data.Courses); })
result.Add(student); .ToList();
}
return result;
} }
}
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();
}
}
}