Enhance staffing management in school simulation by introducing API endpoints for hiring staff and assigning subjects. Implement payroll cap validation to ensure hiring and subject assignments do not exceed the allocated budget. Update the simulation options to include a monthly payroll cap and revise related classes to support new staffing functionalities. Enhance documentation to reflect these changes and update tests to validate the new features.

This commit is contained in:
Leonid Pershin
2026-08-19 00:19:09 +03:00
parent c189578680
commit 94842ab192
24 changed files with 1444 additions and 27 deletions
+120
View File
@@ -169,3 +169,123 @@ internal static class PeopleListMapper
return new PeopleFilterOptionsResponse(years, letters, positions);
}
}
internal sealed record HireStaffRequest(string? PersonId, string? Position);
internal sealed record AssignSubjectRequest(string? Subject);
internal sealed record StaffingResponse(
float Allocated,
float Payroll,
float Remaining,
IReadOnlyList<UncoveredSubjectResponse> Uncovered,
IReadOnlyList<ApplicantResponse> Applicants,
IReadOnlyList<StaffMemberResponse> Staff);
internal sealed record UncoveredSubjectResponse(
string DefName,
string Label,
int GradeMin,
int GradeMax,
int HoursPerWeek);
internal sealed record ApplicantResponse(
string Id,
string FullName,
bool Female,
int Age,
bool IsParent,
float HourlyWageAsk,
float MonthlyBase);
internal sealed record StaffMemberResponse(
string Id,
string FullName,
bool Female,
int Age,
bool IsParent,
string Position,
string PositionLabel,
float HourlyWageAsk,
float MonthlyPay,
IReadOnlyList<DefLabelResponse> Subjects);
internal static class StaffingMapper
{
public static StaffingResponse From(
Roster? roster,
ApplicantPool? pool,
DefCatalog? catalog,
DateTime asOf,
float allocated,
string locale)
{
roster ??= new Roster([], [], []);
pool ??= ApplicantPool.Empty;
var rules = catalog?.StaffingRules;
var payroll = rules is null ? 0f : Staffing.Payroll(rules, roster.People);
var remaining = MathF.Round(MathF.Max(0f, allocated - payroll), 2);
var uncovered = catalog is null
? Array.Empty<UncoveredSubjectResponse>()
: Staffing.Uncovered(catalog, roster)
.Select(subject => new UncoveredSubjectResponse(
subject.DefName,
catalog.Label(locale, subject),
subject.Grades.Min,
subject.Grades.Max,
subject.HoursPerWeek))
.ToArray();
var applicants = pool.Applicants
.Select(applicant => new ApplicantResponse(
applicant.Person.Id,
applicant.Person.Name.Full,
applicant.Person.Female,
applicant.Person.AgeOn(asOf),
applicant.Person.IsParent,
applicant.HourlyWageAsk,
rules is null ? 0f : Staffing.MonthlyBase(rules, applicant.HourlyWageAsk)))
.ToArray();
var staff = roster.People
.Where(person => person.IsStaff)
.OrderBy(person => person.Name.Surname, StringComparer.Ordinal)
.ThenBy(person => person.Id, StringComparer.Ordinal)
.Select(person => Member(person, catalog, rules, locale, asOf))
.ToArray();
return new StaffingResponse(allocated, payroll, remaining, uncovered, applicants, staff);
}
private static StaffMemberResponse Member(
Person person,
DefCatalog? catalog,
StaffingDef? rules,
string locale,
DateTime asOf)
{
var ask = person.HourlyWageAsk ?? 0f;
var pay = rules is null ? 0f : Staffing.MonthlyPay(rules, ask, person.Subjects.Count);
var subjects = person.Subjects
.Select(name => new DefLabelResponse(
name,
catalog is not null && catalog.Subjects.TryGetValue(name, out var def)
? catalog.Label(locale, def)
: name))
.ToArray();
return new StaffMemberResponse(
person.Id,
person.Name.Full,
person.Female,
person.AgeOn(asOf),
person.IsParent,
person.Position ?? string.Empty,
PeopleListMapper.PositionLabel(catalog, locale, person.Position) ?? person.Position ?? string.Empty,
ask,
pay,
subjects);
}
}