Today's history is a paged HTTP log on the worker, not on the card or in people.json. Co-authored-by: Cursor <cursoragent@cursor.com>
391 lines
12 KiB
C#
391 lines
12 KiB
C#
using HSchool.Content;
|
|
using HSchool.People;
|
|
|
|
namespace HSchool.Server.Api;
|
|
|
|
internal enum PersonLookupError
|
|
{
|
|
None,
|
|
UnknownSchool,
|
|
UnknownPerson,
|
|
}
|
|
|
|
internal sealed record PersonCardResult(PersonCardResponse? Card, PersonLookupError Error);
|
|
|
|
internal sealed record PersonLogResult(PersonLogResponse? Page, PersonLookupError Error);
|
|
|
|
internal sealed record PersonLogResponse(
|
|
int Total,
|
|
int Page,
|
|
int PageSize,
|
|
IReadOnlyList<PersonLogEntryResponse> Entries);
|
|
|
|
internal sealed record PersonLogEntryResponse(DateTime Time, string Type, string Label, string? ThingDef);
|
|
|
|
internal sealed record PeopleListResponse(
|
|
int Total,
|
|
int Page,
|
|
int PageSize,
|
|
IReadOnlyList<PersonListItemResponse> People,
|
|
PeopleFilterOptionsResponse Filters);
|
|
|
|
internal sealed record PeopleFilterOptionsResponse(
|
|
IReadOnlyList<int> Years,
|
|
IReadOnlyList<string> Letters,
|
|
IReadOnlyList<DefLabelResponse> Positions);
|
|
|
|
internal sealed record DefLabelResponse(string DefName, string Label);
|
|
|
|
internal sealed record PersonListItemResponse(
|
|
string Id,
|
|
string FullName,
|
|
string Surname,
|
|
string Given,
|
|
string Patronymic,
|
|
bool Female,
|
|
int Age,
|
|
IReadOnlyList<string> Roles,
|
|
int? ClassYear,
|
|
string? ClassLetter,
|
|
string? Position,
|
|
string? PositionLabel);
|
|
|
|
internal sealed record PersonCardResponse(
|
|
string Id,
|
|
string FullName,
|
|
string Surname,
|
|
string Given,
|
|
string Patronymic,
|
|
bool Female,
|
|
int Age,
|
|
DateTime BirthDate,
|
|
IReadOnlyList<string> Roles,
|
|
int? ClassYear,
|
|
string? ClassLetter,
|
|
string? ClassId,
|
|
string? Position,
|
|
string? PositionLabel,
|
|
IReadOnlyList<LabeledStatResponse> Body,
|
|
IReadOnlyList<LabeledStatResponse> Skills,
|
|
IReadOnlyList<DefLabelResponse> Traits,
|
|
IReadOnlyList<NeedStatResponse> Needs,
|
|
string? Activity,
|
|
string? ActivityLabel,
|
|
PersonFamilyResponse Family,
|
|
IReadOnlyList<WornItemResponse> Worn,
|
|
IReadOnlyList<CarriedItemResponse> Carried,
|
|
float CarryMass,
|
|
float CarryCapacity,
|
|
bool HasLocker,
|
|
int HomeCount);
|
|
|
|
internal sealed record WornItemResponse(
|
|
string DefName,
|
|
string Label,
|
|
string? Color,
|
|
string? ColorLabel,
|
|
IReadOnlyList<DefLabelResponse> Layers,
|
|
float Condition,
|
|
string? ConditionLabel);
|
|
|
|
internal sealed record CarriedItemResponse(
|
|
string DefName,
|
|
string Label,
|
|
string? Color,
|
|
string? ColorLabel,
|
|
string? Subject,
|
|
string? SubjectLabel,
|
|
float Mass);
|
|
|
|
internal sealed record LabeledStatResponse(string Id, string Label, string Value);
|
|
|
|
internal sealed record NeedStatResponse(string Id, string Label, float Value);
|
|
|
|
internal sealed record PersonFamilyResponse(
|
|
IReadOnlyList<PersonRelResponse> Parents,
|
|
IReadOnlyList<PersonRelResponse> Children,
|
|
IReadOnlyList<PersonRelResponse> Siblings,
|
|
IReadOnlyList<PersonRelResponse> Partners);
|
|
|
|
internal sealed record PersonRelResponse(string Id, string FullName, bool Female);
|
|
|
|
internal sealed record DirectoryResponse(IReadOnlyList<DirectoryPersonResponse> People);
|
|
|
|
internal sealed record DirectoryPersonResponse(string Id, string FullName);
|
|
|
|
internal static class PeopleListMapper
|
|
{
|
|
public static PeopleListResponse From(
|
|
Roster roster,
|
|
RosterPage page,
|
|
DateTime asOf,
|
|
DefCatalog? catalog,
|
|
string locale,
|
|
RosterQuery query)
|
|
{
|
|
var classes = roster.Classes.ToDictionary(schoolClass => schoolClass.Id, StringComparer.Ordinal);
|
|
var people = page.People
|
|
.Select(person => Item(person, classes, asOf, catalog, locale))
|
|
.ToArray();
|
|
return new PeopleListResponse(page.Total, query.Page, query.PageSize, people, FilterOptions(roster, catalog, locale));
|
|
}
|
|
|
|
private static PersonListItemResponse Item(
|
|
Person person,
|
|
IReadOnlyDictionary<string, SchoolClass> classes,
|
|
DateTime asOf,
|
|
DefCatalog? catalog,
|
|
string locale)
|
|
{
|
|
int? year = null;
|
|
string? letter = null;
|
|
if (person.ClassId is { } classId && classes.TryGetValue(classId, out var schoolClass))
|
|
{
|
|
year = schoolClass.Year;
|
|
letter = schoolClass.Letter;
|
|
}
|
|
|
|
return new PersonListItemResponse(
|
|
person.Id,
|
|
person.Name.Full,
|
|
person.Name.Surname,
|
|
person.Name.Given,
|
|
person.Name.Patronymic,
|
|
person.Female,
|
|
person.AgeOn(asOf),
|
|
RolesOf(person),
|
|
year,
|
|
letter,
|
|
person.Position,
|
|
PositionLabel(catalog, locale, person.Position));
|
|
}
|
|
|
|
internal static string[] RolesOf(Person person)
|
|
{
|
|
var roles = new List<string>(3);
|
|
if (person.IsStudent)
|
|
{
|
|
roles.Add(PersonRoles.Student);
|
|
}
|
|
|
|
if (person.IsStaff)
|
|
{
|
|
roles.Add(PersonRoles.Staff);
|
|
}
|
|
|
|
if (person.IsParent)
|
|
{
|
|
roles.Add(PersonRoles.Parent);
|
|
}
|
|
|
|
return [.. roles];
|
|
}
|
|
|
|
internal static string? PositionLabel(DefCatalog? catalog, string locale, string? position)
|
|
{
|
|
if (position is null)
|
|
{
|
|
return null;
|
|
}
|
|
|
|
if (catalog is not null && catalog.Positions.TryGetValue(position, out var def))
|
|
{
|
|
return catalog.Label(locale, def);
|
|
}
|
|
|
|
return position;
|
|
}
|
|
|
|
private static PeopleFilterOptionsResponse FilterOptions(Roster roster, DefCatalog? catalog, string locale)
|
|
{
|
|
var years = roster.Classes.Select(schoolClass => schoolClass.Year).Distinct().OrderBy(year => year).ToArray();
|
|
var letters = roster.Classes.Select(schoolClass => schoolClass.Letter).Distinct().OrderBy(letter => letter, StringComparer.Ordinal).ToArray();
|
|
var positions = roster.People
|
|
.Select(person => person.Position)
|
|
.Where(position => position is not null)
|
|
.Distinct(StringComparer.Ordinal)
|
|
.OrderBy(position => PositionLabel(catalog, locale, position) ?? position, StringComparer.Ordinal)
|
|
.Select(position => new DefLabelResponse(position!, PositionLabel(catalog, locale, position) ?? position!))
|
|
.ToArray();
|
|
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,
|
|
IReadOnlyList<DefLabelResponse> Positions,
|
|
IReadOnlyList<UncoveredSubjectResponse> Subjects);
|
|
|
|
internal sealed record UncoveredSubjectResponse(
|
|
string DefName,
|
|
string Label,
|
|
int GradeMin,
|
|
int GradeMax,
|
|
int HoursPerWeek,
|
|
int TeachersShort);
|
|
|
|
internal sealed record ApplicantResponse(
|
|
string Id,
|
|
string FullName,
|
|
bool Female,
|
|
int Age,
|
|
bool IsParent,
|
|
float HourlyWageAsk,
|
|
float MonthlyBase,
|
|
IReadOnlyList<LabeledStatResponse> Skills,
|
|
IReadOnlyList<DefLabelResponse> Traits);
|
|
|
|
internal sealed record StaffMemberResponse(
|
|
string Id,
|
|
string FullName,
|
|
bool Female,
|
|
int Age,
|
|
bool IsParent,
|
|
string Position,
|
|
string PositionLabel,
|
|
float HourlyWageAsk,
|
|
float WeeklyHours,
|
|
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 || catalog is null ? 0f : Staffing.Payroll(catalog, rules, roster);
|
|
var remaining = MathF.Round(MathF.Max(0f, allocated - payroll), 2);
|
|
|
|
var uncovered = catalog is null
|
|
? Array.Empty<UncoveredSubjectResponse>()
|
|
: Staffing.Uncovered(catalog, roster)
|
|
.Select(row => new UncoveredSubjectResponse(
|
|
row.Subject.DefName,
|
|
catalog.Label(locale, row.Subject),
|
|
row.Subject.Grades.Min,
|
|
row.Subject.Grades.Max,
|
|
row.Subject.HoursPerWeek,
|
|
row.TeachersShort))
|
|
.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),
|
|
SkillsOf(applicant.Person, catalog, locale),
|
|
TraitsOf(applicant.Person, catalog, locale)))
|
|
.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, roster, catalog, rules, locale, asOf))
|
|
.ToArray();
|
|
|
|
var positions = catalog is null
|
|
? Array.Empty<DefLabelResponse>()
|
|
: catalog.Positions.Values
|
|
.Where(def => !def.Abstract)
|
|
.Select(def => new DefLabelResponse(def.DefName, catalog.Label(locale, def)))
|
|
.OrderBy(row => row.Label, StringComparer.Ordinal)
|
|
.ToArray();
|
|
|
|
var subjects = catalog is null
|
|
? Array.Empty<UncoveredSubjectResponse>()
|
|
: catalog.Subjects.Values
|
|
.Where(def => !def.Abstract)
|
|
.Select(def => new UncoveredSubjectResponse(
|
|
def.DefName,
|
|
catalog.Label(locale, def),
|
|
def.Grades.Min,
|
|
def.Grades.Max,
|
|
def.HoursPerWeek,
|
|
0))
|
|
.ToArray();
|
|
|
|
return new StaffingResponse(allocated, payroll, remaining, uncovered, applicants, staff, positions, subjects);
|
|
}
|
|
|
|
private static IReadOnlyList<LabeledStatResponse> SkillsOf(Person person, DefCatalog? catalog, string locale)
|
|
{
|
|
return person.Skills
|
|
.OrderByDescending(pair => pair.Value)
|
|
.ThenBy(pair => pair.Key, StringComparer.Ordinal)
|
|
.Select(pair => new LabeledStatResponse(
|
|
pair.Key,
|
|
catalog is not null && catalog.Skills.TryGetValue(pair.Key, out var def)
|
|
? catalog.Label(locale, def)
|
|
: pair.Key,
|
|
pair.Value.ToString()))
|
|
.ToArray();
|
|
}
|
|
|
|
private static IReadOnlyList<DefLabelResponse> TraitsOf(Person person, DefCatalog? catalog, string locale)
|
|
{
|
|
return person.Traits
|
|
.Select(name => new DefLabelResponse(
|
|
name,
|
|
catalog is not null && catalog.Traits.TryGetValue(name, out var def)
|
|
? catalog.Label(locale, def)
|
|
: name))
|
|
.ToArray();
|
|
}
|
|
|
|
private static StaffMemberResponse Member(
|
|
Person person,
|
|
Roster roster,
|
|
DefCatalog? catalog,
|
|
StaffingDef? rules,
|
|
string locale,
|
|
DateTime asOf)
|
|
{
|
|
var ask = person.HourlyWageAsk ?? 0f;
|
|
var hours = catalog is null ? 0f : Staffing.WeeklyHours(catalog, roster, person);
|
|
var pay = rules is null ? 0f : Staffing.MonthlyPay(rules, ask, hours);
|
|
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,
|
|
MathF.Round(hours, 1),
|
|
pay,
|
|
subjects);
|
|
}
|
|
}
|
|
|