[feat:] im- and export (pdf/json)
This commit is contained in:
+57
-12
@@ -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
@@ -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
@@ -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
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user