5 Commits

Author SHA1 Message Date
fierke 66596b530b [feat:] im- and export (pdf/json) 2026-06-04 18:59:01 +02:00
fierke 5ef41b21b0 [gui:] buttons for im- and exporting 2026-06-04 18:58:32 +02:00
fierke c291ed1788 [file:] included fonts 2026-06-04 18:58:03 +02:00
fierke 78d25e6231 [fix:] Sport ids 2026-06-01 21:28:07 +02:00
fierke 73af2039ba [file:] added fonts 2026-06-01 21:27:29 +02:00
12 changed files with 349 additions and 120 deletions
+18 -4
View File
@@ -222,7 +222,7 @@
<Label FontSize="20" Content="Ergebnisse" VerticalContentAlignment="Center" />
</StackPanel>
</TabItem.Header>
<Grid ColumnDefinitions="*,*" RowDefinitions="50,2*,*">
<Grid ColumnDefinitions="*,*" RowDefinitions="100,2*,*">
<ListBox Grid.RowSpan="2" x:Name="LbResult" Margin="0,10,10,10" PointerPressed="LbResult_OnPointerPressed">
<ListBox.ContextMenu>
<ContextMenu>
@@ -230,21 +230,35 @@
</ContextMenu>
</ListBox.ContextMenu>
</ListBox>
<Grid Grid.Row="0" Grid.Column="1" Grid.ColumnDefinitions="*,*">
<Button Grid.Column="0" Margin="0,10,0,0" x:Name="BtnExportCoursePDF" VerticalAlignment="Top" Height="35" HorizontalAlignment="Stretch" Click="BtnExportCoursePDF_OnClick" HorizontalContentAlignment="Center">
<Grid Grid.Row="0" Grid.Column="1" RowDefinitions="*,*" ColumnDefinitions="*,*">
<Button Grid.Row="0" Grid.Column="0" Margin="0,10,5,0" x:Name="BtnExportCoursePDF" VerticalAlignment="Top" Height="35" HorizontalAlignment="Stretch" Click="BtnExportCoursePDF_OnClick" HorizontalContentAlignment="Center">
<StackPanel Orientation="Horizontal">
<LucideIcon Kind="FileText" Width="24" Height="24" />
<Label Content="Export (PDF)..." VerticalContentAlignment="Center" FontSize="12"
FontWeight="Bold" />
</StackPanel>
</Button>
<Button Grid.Column="1" Margin="5,10,0,0" x:Name="BtnExportCourseCSV" VerticalAlignment="Top" Height="35" HorizontalAlignment="Stretch" Click="BtnExportCourseCSV_OnClick" HorizontalContentAlignment="Center">
<Button Grid.Row="0" Grid.Column="1" Margin="5,10,0,0" x:Name="BtnExportCourseCSV" VerticalAlignment="Top" Height="35" HorizontalAlignment="Stretch" Click="BtnExportCourseCSV_OnClick" HorizontalContentAlignment="Center">
<StackPanel Orientation="Horizontal">
<LucideIcon Kind="FileJson" Width="24" Height="24" />
<Label Content="Export (CSV)..." VerticalContentAlignment="Center" FontSize="12"
FontWeight="Bold" />
</StackPanel>
</Button>
<Button Grid.Row="1" Grid.Column="0" Margin="0,5,5,0" x:Name="BtnExportConfiguration" VerticalAlignment="Top" Height="35" HorizontalAlignment="Stretch" Click="MnuExpSettings_OnClick" HorizontalContentAlignment="Center">
<StackPanel Orientation="Horizontal">
<LucideIcon Kind="FileJson" Width="24" Height="24" />
<Label Content="Konfiguration exportieren..." VerticalContentAlignment="Center" FontSize="12"
FontWeight="Bold" />
</StackPanel>
</Button>
<Button Grid.Row="1" Grid.Column="1" Margin="5,5,0,0" x:Name="BtnImportResults" VerticalAlignment="Top" Height="35" HorizontalAlignment="Stretch" Click="MnuImpResult_OnClick" HorizontalContentAlignment="Center">
<StackPanel Orientation="Horizontal">
<LucideIcon Kind="Import" Width="24" Height="24" />
<Label Content="Ergebnisse importieren..." VerticalContentAlignment="Center" FontSize="12"
FontWeight="Bold" />
</StackPanel>
</Button>
</Grid>
<ScrollViewer Grid.Row="1" Grid.Column="1" Margin="0,5,0,10" Background="#44CCCCCC">
<TextBlock x:Name="TbResultStatistics"></TextBlock>
+56 -11
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);
}
}
+9 -1
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()
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)
+48
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);
}
}
+60 -23
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)
return dict
.Select(entry => new Student(entry.Key, string.Empty, new List<string>())
{
var student = new Student(id, data.Name, data.Courses);
result.Add(student);
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();
}
}
}
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
+21
View File
@@ -21,6 +21,21 @@
<PackageReference Include="Lucide.Avalonia" Version="0.1.35"/>
</ItemGroup>
<ItemGroup>
<Reference Include="PdfSharp">
<HintPath>lib/pdfsharp/PdfSharp.dll</HintPath>
<Private>true</Private>
</Reference>
<Reference Include="PdfSharp.System">
<HintPath>lib/pdfsharp/PdfSharp.System.dll</HintPath>
<Private>true</Private>
</Reference>
<Reference Include="Microsoft.Extensions.Logging.Abstractions">
<HintPath>lib/pdfsharp/Microsoft.Extensions.Logging.Abstractions.dll</HintPath>
<Private>true</Private>
</Reference>
</ItemGroup>
<ItemGroup>
<None Remove="res\logo.ico"/>
<AvaloniaResource Include="res\logo.ico">
@@ -32,4 +47,10 @@
</AvaloniaResource>
</ItemGroup>
<ItemGroup>
<None Include="res/fonts/*.ttf">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</None>
</ItemGroup>
</Project>
+16 -13
View File
@@ -99,19 +99,22 @@ public class Settings
public static void ImportInitial()
{
Instance.Sports.Add(new Sport("Tischtennis"){ AlternativeNames = {"Sport_TT"}});
Instance.Sports.Add(new Sport("Badminton"){ AlternativeNames = {"Sport_BM"}});
Instance.Sports.Add(new Sport("Gymnastik/Tanz"){ AlternativeNames = {"Sport_Gym"}});
Instance.Sports.Add(new Sport("Schwimmen"){ AlternativeNames = {"Sport_SW"}, Semester = [1, 1, 1, 1], MaxStudents = 18});
Instance.Sports.Add(new Sport("Bouldern"){ AlternativeNames = {"Sport_BO"}, Semester = [1, 1, 1, 1]});
Instance.Sports.Add(new Sport("Basketball"){ AlternativeNames = {"Sport_BS"}});
Instance.Sports.Add(new Sport("Fitness"){ AlternativeNames = {"Sport_Fit"}});
Instance.Sports.Add(new Sport("Fußball"){ AlternativeNames = {"Sport_Fuß"}, Semester = [1, 0, 1, 0]});
Instance.Sports.Add(new Sport("Handball"){ AlternativeNames = {"Sport_HB"}});
Instance.Sports.Add(new Sport("Leichtathletik"){ AlternativeNames = {"Sport_LA"}, Semester = [1, 0, 1, 0], MaxStudents = 18});
Instance.Sports.Add(new Sport("Tennis"){ AlternativeNames = {"Sport_Te"}});
Instance.Sports.Add(new Sport("Turnen"){ AlternativeNames = {"Sport_Tur"}});
Instance.Sports.Add(new Sport("Volleyball"){ AlternativeNames = {"Sport_VB"}});
Instance.Sports.Clear();
int id = 1;
Instance.Sports.Add(new Sport("Tischtennis"){ ID = id++, AlternativeNames = {"Sport_TT"}});
Instance.Sports.Add(new Sport("Badminton"){ ID = id++, AlternativeNames = {"Sport_BM"}});
Instance.Sports.Add(new Sport("Gymnastik/Tanz"){ ID = id++, AlternativeNames = {"Sport_Gym"}});
Instance.Sports.Add(new Sport("Schwimmen"){ ID = id++, AlternativeNames = {"Sport_SW"}, Semester = [1, 1, 1, 1], MaxStudents = 18});
Instance.Sports.Add(new Sport("Bouldern"){ ID = id++, AlternativeNames = {"Sport_BO"}, Semester = [1, 1, 1, 1]});
Instance.Sports.Add(new Sport("Basketball"){ ID = id++, AlternativeNames = {"Sport_BS"}});
Instance.Sports.Add(new Sport("Fitness"){ ID = id++, AlternativeNames = {"Sport_Fit"}});
Instance.Sports.Add(new Sport("Fußball"){ ID = id++, AlternativeNames = {"Sport_Fuß"}, Semester = [1, 0, 1, 0]});
Instance.Sports.Add(new Sport("Handball"){ ID = id++, AlternativeNames = {"Sport_HB"}});
Instance.Sports.Add(new Sport("Leichtathletik"){ ID = id++, AlternativeNames = {"Sport_LA"}, Semester = [1, 0, 1, 0], MaxStudents = 18});
Instance.Sports.Add(new Sport("Tennis"){ ID = id++, AlternativeNames = {"Sport_Te"}});
Instance.Sports.Add(new Sport("Turnen"){ ID = id++, AlternativeNames = {"Sport_Tur"}});
Instance.Sports.Add(new Sport("Volleyball"){ ID = id++, AlternativeNames = {"Sport_VB"}});
}
public static string? GetSportNameFromID(int id)