6 Commits

Author SHA1 Message Date
fierke ab133cd267 [file:] better image 2026-06-12 22:20:28 +02:00
fierke f31e6d2813 [file:] better image 2026-06-12 22:11:51 +02:00
fierke 08db1eb681 [fix:] first semester priority 2026-06-07 20:31:30 +02:00
fierke 9be546d25f [feat:] overview of generated courses (pdf) 2026-06-07 20:19:54 +02:00
fierke 2910b1aeda Merge remote-tracking branch 'origin/main' 2026-06-07 12:51:51 +02:00
fierke 7a0e392ba8 [chore:] removed comments 2026-06-07 12:51:45 +02:00
5 changed files with 185 additions and 18 deletions
+101 -1
View File
@@ -38,6 +38,8 @@ public static class PdfExportUtility
return;
}
RenderCoursesOverview(document, courses);
var studentsById = Settings.Instance.Students
.GroupBy(student => student.ID, StringComparer.OrdinalIgnoreCase)
.ToDictionary(group => group.Key, group => group.First(), StringComparer.OrdinalIgnoreCase);
@@ -109,7 +111,7 @@ public static class PdfExportUtility
if (pageIndex == 0)
{
gfx.DrawString("Inkludiert nur SuS, die in mindestens einem Semester keine Zuordnung erhalten haben.",
gfx.DrawString("Es werden nur Schülerinnen und Schüler aufgeführt, die in mindestens einem Semester keine Zuordnung erhalten haben.",
subtitleFont, XBrushes.Gray, new XRect(Margin, y, contentWidth, 20), XStringFormats.TopLeft);
y += 24;
}
@@ -178,6 +180,104 @@ public static class PdfExportUtility
DrawFooter(gfx, page);
}
private static void RenderCoursesOverview(
PdfDocument document,
IReadOnlyList<(int Semester, CourseCrafter.CourseInstance Instance)> courses)
{
var overviewFont = new XFont(FontFamily, 18, XFontStyleEx.Bold);
var subtitleFont = new XFont(FontFamily, 10, XFontStyleEx.Regular);
var tableHeaderFont = new XFont(FontFamily, 9, XFontStyleEx.Bold);
var tableBodyFont = new XFont(FontFamily, 8.5, XFontStyleEx.Regular);
int count = 0;
var coursesPerSemester = courses
.GroupBy(course => course.Semester)
.ToDictionary(group => group.Key, group => group.Count());
var courseIndexBySemester = new Dictionary<int, int>();
var rows = courses.Select(course =>
{
count++;
var key = course.Semester;
courseIndexBySemester.TryGetValue(key, out var index);
index++;
courseIndexBySemester[key] = index;
return new[]
{
count.ToString(),
course.Semester.ToString(),
course.Instance.Sport.Name,
$"{index}/{coursesPerSemester[key]}",
course.Instance.Students.Count.ToString()
};
}).ToList();
var columns = new[] { 50.0, 70.0, 150.0, 150.0, 100.0 };
var headerRow = new[]
{
"Nr.",
"Semester",
"Sportart",
"Kurs (Semester)",
"Anzahl SuS"
};
int rowIndex = 0;
int pageIndex = 0;
while (rowIndex < rows.Count || pageIndex == 0)
{
var page = document.AddPage();
ConfigurePage(page);
using var gfx = XGraphics.FromPdfPage(page);
double pageWidth = page.Width.Point;
double pageHeight = page.Height.Point;
double contentWidth = pageWidth - (Margin * 2);
double contentBottom = pageHeight - Margin - FooterHeight;
double y = Margin;
var headerTitle = pageIndex == 0
? "Übersicht generierter Kurse"
: "Fortsetzung: Übersicht generierter Kurse";
gfx.DrawString(headerTitle, overviewFont, XBrushes.Black,
new XRect(Margin, y, contentWidth, 24), XStringFormats.TopLeft);
y += 26;
if (pageIndex == 0)
{
gfx.DrawString("Tabellarische Übersicht aller generierten Kurse nach Semester und Sportart.",
subtitleFont, XBrushes.Gray, new XRect(Margin, y, contentWidth, 20), XStringFormats.TopLeft);
y += 24;
}
y += DrawTableRow(gfx, y, columns, headerRow, tableHeaderFont, fillHeader: true);
while (rowIndex < rows.Count)
{
var row = rows[rowIndex];
double rowHeight = MeasureRowHeight(gfx, columns, row, tableBodyFont);
if (y + rowHeight > contentBottom)
break;
y += DrawTableRow(gfx, y, columns, row, tableBodyFont, fillHeader: false);
rowIndex++;
}
DrawFooter(gfx, page);
pageIndex++;
if (rowIndex >= rows.Count)
break;
}
}
private static void RenderCourse(
PdfDocument document,
(int Semester, CourseCrafter.CourseInstance Instance) course,
+84 -1
View File
@@ -744,6 +744,87 @@ public class CourseCrafter
return true;
}
bool EnsureFirstSemesterCoverage()
{
bool changed = false;
foreach (var student in Settings.Instance.Students)
{
if (student.SelectedCourseNames.Count == 0)
continue;
var assignments = GetAssignmentsBySemester(student.ID);
if (assignments[0] != null)
continue;
if (TryAssignStudentToFirstSemester(student))
changed = true;
}
return changed;
}
bool TryAssignStudentToFirstSemester(Student student)
{
var desiredSports = student.SelectedCourseNames
.Select(ResolveSportFromSelection)
.Where(sp => sp != null && sp.Semester[0] != 0)
.DistinctBy(sp => sp!.Name)
.Select(sp => sp!)
.ToList();
foreach (var sport in desiredSports)
{
// Direkt in bestehendes Kursangebot eintragen
var existingCourse = GeneratedCourses
.FirstOrDefault(c => c.Semester == 1 && c.Instance.Sport.Name == sport.Name &&
c.Instance.Students.Count < c.Instance.Sport.MaxStudents);
if (existingCourse.Instance != null)
{
existingCourse.Instance.Students.Add(student.ID);
students_in_semester[0].Add(student.ID);
return true;
}
// Versuche einen Teilnehmer aus einem bestehenden Sem.1-Kurs umzudisponieren
var firstSemesterCourses = GeneratedCourses
.Where(c => c.Semester == 1 && c.Instance.Sport.Name == sport.Name)
.ToList();
foreach (var course in firstSemesterCourses)
{
if (course.Instance.Students.Count <= getEffectiveMinStudents(sport, 1))
continue;
foreach (var otherStudent in course.Instance.Students.ToList())
{
if (otherStudent == student.ID)
continue;
var targetCourse = GeneratedCourses
.FirstOrDefault(c => c.Semester != 1 && c.Instance.Sport.Name == sport.Name &&
c.Instance.Students.Count < c.Instance.Sport.MaxStudents &&
!students_in_semester[c.Semester - 1].Contains(otherStudent));
if (targetCourse.Instance == null)
continue;
course.Instance.Students.Remove(otherStudent);
students_in_semester[0].Remove(otherStudent);
targetCourse.Instance.Students.Add(otherStudent);
students_in_semester[targetCourse.Semester - 1].Add(otherStudent);
course.Instance.Students.Add(student.ID);
students_in_semester[0].Add(student.ID);
return true;
}
}
}
return false;
}
(int Semester, CourseInstance Instance)?[] GetAssignmentsBySemester(string studentId)
{
var assignments = new (int Semester, CourseInstance Instance)?[4];
@@ -800,7 +881,8 @@ public class CourseCrafter
if (freeInterestedStudents > maxFreeInterestedStudents ||
(freeInterestedStudents == maxFreeInterestedStudents &&
totalCoursesPerSemester[i] < minCourses))
(totalCoursesPerSemester[i] < minCourses ||
(totalCoursesPerSemester[i] == minCourses && semesterNumber < bestSem))))
{
maxFreeInterestedStudents = freeInterestedStudents;
minCourses = totalCoursesPerSemester[i];
@@ -811,6 +893,7 @@ public class CourseCrafter
return bestSem;
}
EnsureFirstSemesterCoverage();
ReloadResult();
}
Binary file not shown.

Before

Width:  |  Height:  |  Size: 82 KiB

After

Width:  |  Height:  |  Size: 141 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 194 KiB

After

Width:  |  Height:  |  Size: 207 KiB

-16
View File
@@ -20,25 +20,9 @@
</PackageReference>
<PackageReference Include="Lucide.Avalonia" Version="0.1.35"/>
<PackageReference Include="PdfSharp" Version="6.2.4"/>
<!-- <PackageReference Include="PdfSharp.System" Version="6.2.4"/> -->
<PackageReference Include="Microsoft.Extensions.Logging.Abstractions" Version="10.0.8"/>
</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">