Merge remote-tracking branch 'origin/main'

This commit is contained in:
2026-02-28 18:26:12 +01:00
4 changed files with 440 additions and 20 deletions

View File

@@ -20,7 +20,7 @@
<MenuItem Header="Über" x:Name="MnuAbout" Click="MnuAbout_OnClick" /> <MenuItem Header="Über" x:Name="MnuAbout" Click="MnuAbout_OnClick" />
</MenuItem> </MenuItem>
</Menu> </Menu>
<TabControl Grid.Row="1"> <TabControl x:Name="TclMainView" Grid.Row="1">
<TabItem> <TabItem>
<TabItem.Header> <TabItem.Header>
<StackPanel Orientation="Horizontal"> <StackPanel Orientation="Horizontal">
@@ -100,22 +100,112 @@
<Label FontSize="20" Content="Kurse" VerticalContentAlignment="Center" /> <Label FontSize="20" Content="Kurse" VerticalContentAlignment="Center" />
</StackPanel> </StackPanel>
</TabItem.Header> </TabItem.Header>
<Grid RowDefinitions="2*,*,*"> <Grid ColumnDefinitions="*,2*">
<StackPanel Grid.ColumnSpan="2" Orientation="Horizontal">
<Button Margin="0,10,0,0" x:Name="BtnImportDefaultCourses" VerticalAlignment="Top" Height="30" HorizontalAlignment="Stretch" Click="BtnImportDefaultCourses_OnClick" HorizontalContentAlignment="Center">
<StackPanel Orientation="Horizontal">
<LucideIcon Kind="Import" Width="24" Height="24" />
<Label Content="Standardkurse importieren..." VerticalContentAlignment="Center" FontSize="12"
FontWeight="Bold" />
</StackPanel>
</Button>
</StackPanel>
<ListBox Grid.Column="0" x:Name="LbSportCourses" SelectionChanged="LbSportCourses_OnSelectionChanged" Margin="0,50,0,0" />
<StackPanel Grid.Column="1" Grid.Row="0" Margin="10,10,10,10" Orientation="Vertical" Spacing="10">
<Grid ColumnDefinitions="*,3*">
<Label Content="ID"></Label>
<Label Grid.Column="1" x:Name="LblSportID"></Label>
</Grid>
<Grid ColumnDefinitions="*,3*">
<Label Content="Name"></Label>
<TextBox Grid.Column="1" x:Name="TbSportName" TextChanged="TbStudentID_OnTextChanged"></TextBox>
</Grid>
<Grid ColumnDefinitions="*,3*">
<Label Content="Maximale Schüler*innenanzahl"></Label>
<NumericUpDown Grid.Column="1" x:Name="NudSportMaxStudents"></NumericUpDown>
</Grid>
<Grid ColumnDefinitions="*,3*">
<Label Content="Minimale Schüler*innenanzahl"></Label>
<NumericUpDown Grid.Column="1" x:Name="NudSportMinStudents"></NumericUpDown>
</Grid>
<Grid ColumnDefinitions="*,3*">
<Label Content="Anzahl Angebote Semester 1"></Label>
<NumericUpDown Grid.Column="1" x:Name="NudAmountCoursesSem1"></NumericUpDown>
</Grid>
<Grid ColumnDefinitions="*,3*">
<Label Content="Anzahl Angebote Semester 2"></Label>
<NumericUpDown Grid.Column="1" x:Name="NudAmountCoursesSem2"></NumericUpDown>
</Grid>
<Grid ColumnDefinitions="*,3*">
<Label Content="Anzahl Angebote Semester 3"></Label>
<NumericUpDown Grid.Column="1" x:Name="NudAmountCoursesSem3"></NumericUpDown>
</Grid>
<Grid ColumnDefinitions="*,3*">
<Label Content="Anzahl Angebote Semester 4"></Label>
<NumericUpDown Grid.Column="1" x:Name="NudAmountCoursesSem4"></NumericUpDown>
</Grid>
<Grid ColumnDefinitions="*,3*">
<Label Content="Alternativkurse"></Label>
<StackPanel Grid.Column="1" Orientation="Vertical">
<Grid ColumnDefinitions="*,50">
<ComboBox Height="35" HorizontalAlignment="Stretch" x:Name="CbAlternativCourse"></ComboBox>
<Button Grid.Column="1" Margin="0,10,0,0" x:Name="BtnAlternativeCourseAdd" VerticalAlignment="Top" Height="35" HorizontalAlignment="Stretch" Click="BtnImportDefaultCourses_OnClick" HorizontalContentAlignment="Center">
<StackPanel Orientation="Horizontal">
<LucideIcon Kind="Plus" Width="24" Height="24" />
</StackPanel>
</Button>
</Grid>
<ListBox x:Name="LbAlternativeCourses">
<ContextMenu>
<MenuItem x:Name="MnuAlternativeCoursesDelete" Header="Entfernen"></MenuItem>
</ContextMenu>
</ListBox>
</StackPanel>
</Grid>
<Grid ColumnDefinitions="*,3*">
<Label Content="Alternativbezeichnungen"></Label>
<StackPanel Grid.Column="1" Orientation="Vertical">
<Grid ColumnDefinitions="*,50">
<TextBox Grid.Column="0" Height="35" HorizontalAlignment="Stretch" x:Name="TbSportAlternativeName" TextChanged="TbStudentID_OnTextChanged"></TextBox>
<Button Grid.Column="1" Margin="0,10,0,0" x:Name="BtnAlternativeNameAdd" VerticalAlignment="Top" Height="35" HorizontalAlignment="Stretch" Click="BtnImportDefaultCourses_OnClick" HorizontalContentAlignment="Center">
<StackPanel Orientation="Horizontal">
<LucideIcon Kind="Plus" Width="24" Height="24" />
</StackPanel>
</Button>
</Grid>
<ListBox x:Name="LbAlternativeNames">
<ContextMenu>
<MenuItem x:Name="MnuAlternativeNameDelete" Header="Entfernen"></MenuItem>
</ContextMenu>
</ListBox>
</StackPanel>
</Grid>
</StackPanel>
</Grid> </Grid>
</TabItem> </TabItem>
<TabItem> <TabItem x:Name="TbiResults">
<TabItem.Header> <TabItem.Header>
<StackPanel Orientation="Horizontal"> <StackPanel Orientation="Horizontal">
<LucideIcon Kind="Sparkles" Width="32" Height="32" Size="32" /> <LucideIcon Kind="Sparkles" Width="32" Height="32" Size="32" />
<Label FontSize="20" Content="Ergebnisse" VerticalContentAlignment="Center" /> <Label FontSize="20" Content="Ergebnisse" VerticalContentAlignment="Center" />
</StackPanel> </StackPanel>
</TabItem.Header> </TabItem.Header>
<Grid RowDefinitions="2*,*,*"> <Grid ColumnDefinitions="*,*">
<ListBox x:Name="LbResult" Margin="10,10,10,10"></ListBox>
<ScrollViewer Grid.Column="1">
<TextBlock x:Name="TbResultStatistics"></TextBlock>
</ScrollViewer>
</Grid> </Grid>
</TabItem> </TabItem>
</TabControl> </TabControl>

View File

@@ -2,6 +2,7 @@ using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Diagnostics; using System.Diagnostics;
using Avalonia.Controls; using Avalonia.Controls;
using Avalonia.Input;
using Avalonia.Interactivity; using Avalonia.Interactivity;
using Avalonia.Platform.Storage; using Avalonia.Platform.Storage;
@@ -119,6 +120,30 @@ public partial class MainWindow : Window
private void BtnCraftCourses_OnClick(object? sender, RoutedEventArgs e) private void BtnCraftCourses_OnClick(object? sender, RoutedEventArgs e)
{ {
// Craft courses here / call course-crafter // Craft courses here / call course-crafter
CourseCrafter.Craft();
RefreshResultView();
TbiResults.Focus();
}
private void RefreshResultView()
{
LbResult.Items.Clear();
foreach (Student s in Settings.Instance.Students)
{
try
{
for(int i = 0; i<s.Result.Count;i++)
{
LbResult.Items.Add($"{s.Name} ({s.ID}) - {i+1}. Semester: {s.Result[i]}");
}
}
catch (Exception ex)
{
Console.WriteLine(ex.StackTrace);
}
}
TbResultStatistics.Text = CourseCrafter.GenerateStatistics();
} }
private void LbStudentsImported_OnSelectionChanged(object? sender, SelectionChangedEventArgs e) private void LbStudentsImported_OnSelectionChanged(object? sender, SelectionChangedEventArgs e)
@@ -172,9 +197,6 @@ public partial class MainWindow : Window
try try
{ {
((Student)LbStudentsImported.SelectedItem).Name = TbStudentName.Text; ((Student)LbStudentsImported.SelectedItem).Name = TbStudentName.Text;
//int current = LbStudentsImported.SelectedIndex;
//RefreshImportedStudentList();
//LbStudentsImported.SelectedIndex = current;
} }
catch catch
{ {
@@ -186,13 +208,54 @@ public partial class MainWindow : Window
try try
{ {
((Student)LbStudentsImported.SelectedItem).Name = TbStudentID.Text; ((Student)LbStudentsImported.SelectedItem).Name = TbStudentID.Text;
//int current = LbStudentsImported.SelectedIndex;
//RefreshImportedStudentList();
//LbStudentsImported.SelectedIndex = current;
} }
catch catch
{ {
} }
} }
private void BtnImportDefaultCourses_OnClick(object? sender, RoutedEventArgs e)
{
Settings.ImportInitial();
RefreshCoursesList();
}
private void RefreshCoursesList()
{
LbSportCourses.Items.Clear();
foreach (var sp in Settings.Instance.Sports)
{
LbSportCourses.Items.Add(sp);
}
}
private void LbSportCourses_OnSelectionChanged(object? sender, SelectionChangedEventArgs e)
{
if (LbSportCourses.SelectedItem != null)
{
if (LbSportCourses.SelectedItem is Sport item)
{
LblSportID.Content = item.ID;
TbSportName.Text = item.Name;
NudSportMaxStudents.Value = item.MaxStudents;
NudSportMinStudents.Value = item.MinStudents;
NudAmountCoursesSem1.Value = item.Semester[0];
NudAmountCoursesSem2.Value = item.Semester[1];
NudAmountCoursesSem3.Value = item.Semester[2];
NudAmountCoursesSem4.Value = item.Semester[3];
LbAlternativeCourses.Items.Clear();
foreach (var alternative in item.AlternativeCourses)
{
LbAlternativeCourses.Items.Add(Settings.GetSportNameFromID(alternative));
}
LbAlternativeNames.Items.Clear();
foreach (var alternative in item.AlternativeNames)
{
LbAlternativeNames.Items.Add(alternative);
}
}
}
}
} }

213
crafter.cs Normal file
View File

@@ -0,0 +1,213 @@
using System;
using System.Collections.Generic;
using System.Linq;
namespace spplus;
public class CourseCrafter
{
private class CourseInstance
{
public Sport Sport = null!;
public int Remaining;
public List<Student> Students = new();
}
public static void Craft()
{
var settings = Settings.Instance;
var students = settings.Students;
var sports = settings.Sports;
if (students.Count == 0 || sports.Count == 0)
return;
int semesterCount = sports.Max(s => s.Semester.Length);
// Result initialisieren
foreach (var st in students)
st.Result = Enumerable.Repeat("Fehler", semesterCount).ToList();
// Mapping Name + Alternativen → Sport
var nameMap = new Dictionary<string, Sport>(StringComparer.OrdinalIgnoreCase);
foreach (var sp in sports)
{
nameMap[sp.Name] = sp;
foreach (var alt in sp.AlternativeNames)
nameMap[alt] = sp;
}
// ===== Semesterweise Planung =====
for (int sem = 0; sem < semesterCount; sem++)
{
int remainingCourseSlots = settings.NumCoursesPerSemester;
// Interessenten dieses Semesters
var interested = new Dictionary<Sport, List<Student>>();
foreach (var sp in sports)
interested[sp] = new List<Student>();
foreach (var st in students)
{
if (st.SelectedCourseNames.Count <= sem)
continue;
var sel = st.SelectedCourseNames[sem];
if (nameMap.TryGetValue(sel, out var sp))
interested[sp].Add(st);
}
var instances = new List<CourseInstance>();
// ===== Kurse erzeugen =====
foreach (var sp in sports)
{
if (sem >= sp.Semester.Length)
continue;
int demand = interested[sp].Count;
if (demand < sp.MinStudents)
continue;
int needed = (int)Math.Ceiling(demand / (double)sp.MaxStudents);
int allowed = Math.Min(needed, sp.Semester[sem]);
allowed = Math.Min(allowed, remainingCourseSlots);
if (allowed <= 0)
continue;
remainingCourseSlots -= allowed;
for (int i = 0; i < allowed; i++)
{
instances.Add(new CourseInstance
{
Sport = sp,
Remaining = sp.MaxStudents
});
}
}
// ===== Schüler verteilen =====
foreach (var sp in sports)
{
var pool = interested[sp];
if (pool.Count == 0)
continue;
var targets = instances.Where(i => i.Sport == sp).ToList();
if (targets.Count == 0)
continue;
int idx = 0;
foreach (var st in pool)
{
if (st.SelectedCourseNames.Count <= sem)
continue;
if (st.Result![sem] != "Fehler")
continue;
bool assigned = false;
for (int t = 0; t < targets.Count; t++)
{
var inst = targets[(idx + t) % targets.Count];
if (inst.Remaining > 0)
{
inst.Remaining--;
inst.Students.Add(st);
st.Result[sem] = sp.Name;
idx++;
assigned = true;
break;
}
}
if (!assigned)
continue;
}
}
// ===== Alternativen =====
foreach (var st in students)
{
if (st.SelectedCourseNames.Count <= sem)
continue;
if (st.Result![sem] != "Fehler")
continue;
var sel = st.SelectedCourseNames[sem];
if (!nameMap.TryGetValue(sel, out var wanted))
continue;
foreach (var altId in wanted.AlternativeCourses)
{
var altSport = sports.FirstOrDefault(s => s.ID == altId);
if (altSport == null)
continue;
var inst = instances.FirstOrDefault(i => i.Sport == altSport && i.Remaining > 0);
if (inst == null)
continue;
inst.Remaining--;
inst.Students.Add(st);
st.Result[sem] = altSport.Name;
break;
}
}
}
}
public static string GenerateStatistics()
{
var settings = Settings.Instance;
var students = settings.Students;
var sports = settings.Sports;
if (students.Count == 0 || sports.Count == 0)
return "Keine Daten vorhanden.";
int semesterCount = sports.Max(s => s.Semester.Length);
// Kurszählung: (Semester, SportName) → Liste Schüler
var dict = new Dictionary<(int, string), List<Student>>();
foreach (var st in students)
{
if (st.Result == null)
continue;
for (int sem = 0; sem < st.Result.Count; sem++)
{
var course = st.Result[sem];
if (string.IsNullOrWhiteSpace(course) || course == "Fehler")
continue;
var key = (sem, course);
if (!dict.ContainsKey(key))
dict[key] = new List<Student>();
dict[key].Add(st);
}
}
var sb = new System.Text.StringBuilder();
sb.AppendLine($"Anzahl generierter Kurse: {dict.Count}");
sb.AppendLine("Übersicht:");
foreach (var entry in dict.OrderBy(e => e.Key.Item1).ThenBy(e => e.Key.Item2))
{
int semester = entry.Key.Item1 + 1;
string name = entry.Key.Item2;
int count = entry.Value.Count;
sb.AppendLine($"Semester {semester}: {name} — {count} Schüler*innen");
}
return sb.ToString();
}
}

View File

@@ -1,32 +1,56 @@
using System.Collections.Generic; using System.Collections.Generic;
using Avalonia.Data;
namespace spplus; namespace spplus;
public class Sport public class Sport
{ {
public int ID { get; set; } = 0;
public string Name { get; set; } = "Neuer Kurs"; // Kursname public string Name { get; set; } = "Neuer Kurs"; // Kursname
public int MaxCoursesPerSemester { get; set; } = 1; // Maximale Anzahl an Kursen pro Semester
public int MaxStudents { get; set; } = 20; // Maximale Anzahl an Schülern pro Kurs public int MaxStudents { get; set; } = 25; // Maximale Anzahl an Schülern pro Kurs
public int MinStudents { get; set; } = 5; // Minimale Anzahl an Schülern pro Kurs public int MinStudents { get; set; } = 5; // Minimale Anzahl an Schülern pro Kurs
public int[] Semester { get; set; } = [1, 2, 3, 4]; // Angebot in diesen Semestern public int[] Semester { get; set; } = [2, 2, 2, 2]; // Maximale Anzahl an Angeboten in den jeweiligen Index-Semestern (0 => 1. Semester)
public List<string> AlternativeNames { get; set; } = new();
public List<int> AlternativeCourses { get; set; } = new();
protected Sport() protected Sport()
{ {
} }
protected Sport(string name) public Sport(string name)
{ {
Name = name; Name = name;
} }
protected Sport(string name, int maxCoursesPerSemester, int maxStudents, int minStudents, int[] semester) public Sport(string name, int maxCoursesPerSemester, int maxStudents, int minStudents, int[] semester, List<string> alternativeNames)
{ {
Name = name; Name = name;
MaxCoursesPerSemester = maxCoursesPerSemester;
MaxStudents = maxStudents; MaxStudents = maxStudents;
MinStudents = minStudents; MinStudents = minStudents;
Semester = semester; Semester = semester;
AlternativeNames = alternativeNames;
}
public void AddAlternativeSport(int id)
{
AlternativeCourses.Add(id);
}
public void ClearAlternativeSport()
{
AlternativeCourses.Clear();
}
public override string ToString()
{
var alt = "";
foreach (var s in AlternativeNames)
{
alt += s + ", ";
}
return $"{Name} ({alt})";
} }
} }
@@ -34,7 +58,6 @@ public class Student
{ {
public string ID { get; set; } = ""; // ID des Schüler (z.B. NolteSeb) public string ID { get; set; } = ""; // ID des Schüler (z.B. NolteSeb)
public string Name { get; set; } = ""; // Name des Schülers public string Name { get; set; } = ""; // Name des Schülers
public Sport[] SelectedCourses { get; set; } = new Sport[4]; // Kurswahl
public List<string> SelectedCourseNames { get; set; } = new(); public List<string> SelectedCourseNames { get; set; } = new();
public List<string>? Result { get; set; } = null; public List<string>? Result { get; set; } = null;
@@ -62,6 +85,7 @@ public class Settings
public List<Student> Students { get; set; } = []; public List<Student> Students { get; set; } = [];
public List<Sport> Sports { get; set; } = []; public List<Sport> Sports { get; set; } = [];
public int NumCoursesPerSemester { get; set; } = 10;
public Settings() public Settings()
{ {
@@ -72,4 +96,34 @@ public class Settings
{ {
// Hier importieren... // Hier importieren...
} }
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"}});
}
public static string? GetSportNameFromID(int id)
{
foreach (var s in Instance.Sports)
{
if (s.ID == id)
{
return s.Name;
}
}
return null;
}
} }