Update staffing management to enhance payroll and hiring functionalities
ci / server (push) Failing after 3m39s
ci / client (push) Successful in 14s

- Increased the `MonthlyPayrollCap` from 10,000 to 40,000, allowing for greater flexibility in hiring and subject assignments.
- Revised payroll calculation logic to ensure that staff members are compensated based on their actual weekly hours, with a minimum payment reflecting one full rate.
- Updated documentation to clarify the new payroll structure and its implications for hiring and subject assignments.
- Enhanced tests to validate the new payroll cap and ensure proper functionality in staffing scenarios, including the handling of uncovered subjects.
- Improved localization strings to reflect changes in staffing and payroll terminology.
This commit is contained in:
Leonid Pershin
2026-08-19 12:51:38 +03:00
parent d61240d5c1
commit 25ce26064d
20 changed files with 376 additions and 112 deletions
+44 -8
View File
@@ -56,14 +56,7 @@ public static class TimetablePlanner
demand.Hours,
placed);
if (leftover > 0)
{
demand.Hours = leftover;
}
else
{
demand.Hours = 0;
}
demand.Hours = leftover;
}
var uncovered = remaining
@@ -162,6 +155,14 @@ public static class TimetablePlanner
return leftover;
}
/// <summary>
/// Picks the slot, and the order days are tried in is the whole difference between a timetable
/// and a pile of lessons. Scanning day 0 upwards and taking the first free slot produced blocks
/// of the same subject and a week that ran out by Thursday: Monday held 77 lessons, Friday 16.
///
/// So days are ranked instead: first those that do not have this subject yet, then the emptiest
/// one for this class. Periods stay ascending inside the chosen day, so a day has no holes.
/// </summary>
private static bool TryPlaceOne(
Occupancy occupancy,
string classId,
@@ -172,7 +173,26 @@ public static class TimetablePlanner
int lessonCount,
List<LessonPlacement> placed)
{
var days = new int[weekDays];
for (var day = 0; day < weekDays; day++)
{
days[day] = day;
}
Array.Sort(days, (left, right) =>
{
var bySubject = occupancy.SubjectLoad(classId, subject, left)
.CompareTo(occupancy.SubjectLoad(classId, subject, right));
if (bySubject != 0)
{
return bySubject;
}
var byDay = occupancy.DayLoad(classId, left).CompareTo(occupancy.DayLoad(classId, right));
return byDay != 0 ? byDay : left.CompareTo(right);
});
foreach (var day in days)
{
for (var period = 1; period <= lessonCount; period++)
{
@@ -313,17 +333,33 @@ public static class TimetablePlanner
private readonly HashSet<(string Id, int Day, int Period)> _classes = [];
private readonly HashSet<(string Id, int Day, int Period)> _teachers = [];
private readonly HashSet<(string Id, int Day, int Period)> _rooms = [];
private readonly Dictionary<(string ClassId, int Day), int> _dayLoad = [];
private readonly Dictionary<(string ClassId, string Subject, int Day), int> _subjectLoad = [];
public bool IsFree(string classId, string teacherId, string roomId, int day, int period) =>
!_classes.Contains((classId, day, period))
&& !_teachers.Contains((teacherId, day, period))
&& !_rooms.Contains((roomId, day, period));
/// <summary>How many lessons this class already has that day — counted, not recomputed.</summary>
public int DayLoad(string classId, int day) =>
_dayLoad.TryGetValue((classId, day), out var count) ? count : 0;
/// <summary>How many lessons of this subject this class already has that day.</summary>
public int SubjectLoad(string classId, string subject, int day) =>
_subjectLoad.TryGetValue((classId, subject, day), out var count) ? count : 0;
public void Add(LessonPlacement lesson)
{
_classes.Add((lesson.ClassId, lesson.Day, lesson.Period));
_teachers.Add((lesson.TeacherId, lesson.Day, lesson.Period));
_rooms.Add((lesson.RoomId, lesson.Day, lesson.Period));
var day = (lesson.ClassId, lesson.Day);
_dayLoad[day] = (_dayLoad.TryGetValue(day, out var lessons) ? lessons : 0) + 1;
var subject = (lesson.ClassId, lesson.Subject, lesson.Day);
_subjectLoad[subject] = (_subjectLoad.TryGetValue(subject, out var same) ? same : 0) + 1;
}
}
}