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 Entries); internal sealed record PersonLogEntryResponse(DateTime Time, string Type, string Label, string? ThingDef); internal sealed record PeopleListResponse( int Total, int Page, int PageSize, IReadOnlyList People, PeopleFilterOptionsResponse Filters); internal sealed record PeopleFilterOptionsResponse( IReadOnlyList Years, IReadOnlyList Letters, IReadOnlyList 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 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 Roles, int? ClassYear, string? ClassLetter, string? ClassId, string? Position, string? PositionLabel, IReadOnlyList Body, IReadOnlyList Skills, IReadOnlyList Traits, IReadOnlyList Needs, string? Activity, string? ActivityLabel, PersonFamilyResponse Family, IReadOnlyList Worn, IReadOnlyList Carried, float CarryMass, float CarryCapacity, bool HasLocker, int HomeCount); internal sealed record WornItemResponse( string DefName, string Label, string? Color, string? ColorLabel, IReadOnlyList 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 Parents, IReadOnlyList Children, IReadOnlyList Siblings, IReadOnlyList Partners); internal sealed record PersonRelResponse(string Id, string FullName, bool Female); internal sealed record DirectoryResponse(IReadOnlyList 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 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(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 Uncovered, IReadOnlyList Applicants, IReadOnlyList Staff, IReadOnlyList Positions, IReadOnlyList 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 Skills, IReadOnlyList 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 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() : 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() : 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() : 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 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 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); } }