2 Commits

Author SHA1 Message Date
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
2 changed files with 185 additions and 2 deletions
+101 -1
View File
@@ -38,6 +38,8 @@ public static class PdfExportUtility
return; return;
} }
RenderCoursesOverview(document, courses);
var studentsById = Settings.Instance.Students var studentsById = Settings.Instance.Students
.GroupBy(student => student.ID, StringComparer.OrdinalIgnoreCase) .GroupBy(student => student.ID, StringComparer.OrdinalIgnoreCase)
.ToDictionary(group => group.Key, group => group.First(), StringComparer.OrdinalIgnoreCase); .ToDictionary(group => group.Key, group => group.First(), StringComparer.OrdinalIgnoreCase);
@@ -109,7 +111,7 @@ public static class PdfExportUtility
if (pageIndex == 0) 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); subtitleFont, XBrushes.Gray, new XRect(Margin, y, contentWidth, 20), XStringFormats.TopLeft);
y += 24; y += 24;
} }
@@ -178,6 +180,104 @@ public static class PdfExportUtility
DrawFooter(gfx, page); 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( private static void RenderCourse(
PdfDocument document, PdfDocument document,
(int Semester, CourseCrafter.CourseInstance Instance) course, (int Semester, CourseCrafter.CourseInstance Instance) course,
+84 -1
View File
@@ -744,6 +744,87 @@ public class CourseCrafter
return true; 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) (int Semester, CourseInstance Instance)?[] GetAssignmentsBySemester(string studentId)
{ {
var assignments = new (int Semester, CourseInstance Instance)?[4]; var assignments = new (int Semester, CourseInstance Instance)?[4];
@@ -800,7 +881,8 @@ public class CourseCrafter
if (freeInterestedStudents > maxFreeInterestedStudents || if (freeInterestedStudents > maxFreeInterestedStudents ||
(freeInterestedStudents == maxFreeInterestedStudents && (freeInterestedStudents == maxFreeInterestedStudents &&
totalCoursesPerSemester[i] < minCourses)) (totalCoursesPerSemester[i] < minCourses ||
(totalCoursesPerSemester[i] == minCourses && semesterNumber < bestSem))))
{ {
maxFreeInterestedStudents = freeInterestedStudents; maxFreeInterestedStudents = freeInterestedStudents;
minCourses = totalCoursesPerSemester[i]; minCourses = totalCoursesPerSemester[i];
@@ -811,6 +893,7 @@ public class CourseCrafter
return bestSem; return bestSem;
} }
EnsureFirstSemesterCoverage();
ReloadResult(); ReloadResult();
} }