Merge branch 'phase/36-person-card-tabs'
ci / server (push) Failing after 4m1s
ci / client (push) Failing after 17s

This commit is contained in:
Leonid Pershin
2026-08-20 04:35:35 +03:00
29 changed files with 1458 additions and 280 deletions
+96 -1
View File
@@ -7,7 +7,7 @@ namespace HSchool.Server.Api;
/// <summary>
/// The main menu talks to these: list, create, delete. People list and staffing read published
/// snapshots; the person card, hire and subject changes go through the school's mailbox.
/// snapshots; the person card, the personal log, hire and subject changes go through the school's mailbox.
/// The timetable is a published snapshot; pin/unpin go through the mailbox.
/// </summary>
internal static class SchoolEndpoints
@@ -196,6 +196,41 @@ internal static class SchoolEndpoints
})
.WithName("GetSchoolPerson");
schools.MapGet("/{id:int}/people/{personId}/log", async (
int id,
string personId,
string? q,
string? sort,
string? dir,
int? page,
int? pageSize,
string? lang,
GameCommandQueue commands,
CancellationToken cancellationToken) =>
{
if (string.IsNullOrWhiteSpace(personId) || personId.Length > 64)
{
return Problem(StatusCodes.Status400BadRequest, "invalid-query", "The person id is not valid.");
}
if (!TryParsePersonLogQuery(q, sort, dir, page, pageSize, out var query, out var error))
{
return Problem(StatusCodes.Status400BadRequest, "invalid-query", error);
}
var command = new GameCommand.GetPersonLog(id, personId, ParseLocale(lang), query, NewCompletion<PersonLogResult>());
commands.Enqueue(command);
var outcome = await command.Result.Task.WaitAsync(CommandTimeout, cancellationToken);
return outcome.Error switch
{
PersonLookupError.None when outcome.Page is not null => Results.Ok(outcome.Page),
PersonLookupError.UnknownPerson => Problem(StatusCodes.Status404NotFound, "unknown-person", "That person is not in the school."),
_ => Problem(StatusCodes.Status404NotFound, "unknown-school", "That school does not exist."),
};
})
.WithName("GetSchoolPersonLog");
schools.MapGet("/{id:int}/people/{personId}/portrait", async (
int id,
string personId,
@@ -479,6 +514,66 @@ internal static class SchoolEndpoints
return true;
}
private static bool TryParsePersonLogQuery(
string? search,
string? sort,
string? dir,
int? page,
int? pageSize,
out PersonLogQuery query,
out string error)
{
query = default;
error = string.Empty;
if (search is { Length: > 128 })
{
error = "q must be at most 128 characters.";
return false;
}
if (!string.IsNullOrWhiteSpace(sort) && !sort.Equals("time", StringComparison.OrdinalIgnoreCase))
{
error = "sort must be time.";
return false;
}
var descending = true;
if (!string.IsNullOrWhiteSpace(dir))
{
if (dir.Equals("asc", StringComparison.OrdinalIgnoreCase))
{
descending = false;
}
else if (!dir.Equals("desc", StringComparison.OrdinalIgnoreCase))
{
error = "dir must be asc or desc.";
return false;
}
}
var parsedPage = page ?? 1;
if (parsedPage < 1)
{
error = "page must be 1 or greater.";
return false;
}
var parsedPageSize = pageSize ?? PersonLogBrowser.DefaultPageSize;
if (parsedPageSize < 1 || parsedPageSize > PersonLogBrowser.MaxPageSize)
{
error = $"pageSize must be between 1 and {PersonLogBrowser.MaxPageSize}.";
return false;
}
query = new PersonLogQuery(
string.IsNullOrWhiteSpace(search) ? null : search.Trim(),
descending,
parsedPage,
parsedPageSize);
return true;
}
private static StaffingResponse MapStaffing(PublishedSchoolPeople published, float allocated, string locale) =>
StaffingMapper.From(
published.Roster,