Files
h-school/src/HSchool.Server/Api/TimetableEndpoints.cs
T
Leonid Pershin d61240d5c1
ci / server (push) Failing after 3m31s
ci / client (push) Successful in 13s
Enhance timetable functionality and UI integration
- Updated protocol documentation to include new `classId` and `roomLabel` fields in the timetable API responses.
- Added classes and rooms to the timetable response structure, improving data accessibility for client applications.
- Enhanced the UI components to display timetable information, including class and room details, in the management and people panels.
- Implemented functionality to fetch and display personal timetables for individuals, ensuring a comprehensive view of schedules.
- Revised localization strings to support new timetable features and improve user experience.
- Added tests to validate the new timetable functionalities and ensure robustness in handling timetable data.
2026-08-19 10:45:09 +03:00

190 lines
6.9 KiB
C#

using HSchool.People;
using HSchool.Schedule;
using HSchool.Server.Game;
namespace HSchool.Server.Api;
internal static class TimetableEndpoints
{
private static readonly TimeSpan CommandTimeout = TimeSpan.FromSeconds(5);
public static void MapTimetableEndpoints(this IEndpointRouteBuilder builder)
{
var schools = builder.MapGroup("/api/schools");
schools.MapGet("/{id:int}/timetable", (
int id,
string? classId,
string? personId,
string? lang,
GameLoopService loop) =>
{
var published = loop.FindPeople(id);
if (published is null)
{
return Problem(StatusCodes.Status404NotFound, "unknown-school", "That school does not exist.");
}
return Results.Ok(MapTimetable(published, loop.Options.SchoolWeekDays, ParseLocale(lang), classId, personId));
})
.WithName("GetSchoolTimetable");
schools.MapPost("/{id:int}/timetable/pin", async (
int id,
PinLessonRequest request,
string? lang,
GameCommandQueue commands,
GameLoopService loop,
CancellationToken cancellationToken) =>
{
if (!TryDefName(request.ClassId, "classId", out var classId, out var error)
|| !TryDefName(request.Subject, "subject", out var subject, out error)
|| !TryDefName(request.RoomId, "roomId", out var roomId, out error))
{
return Problem(StatusCodes.Status400BadRequest, "invalid-query", error);
}
var command = new GameCommand.PinLesson(
id,
classId,
subject,
roomId,
request.Day,
request.Period,
NewCompletion<TimetableOutcome>());
commands.Enqueue(command);
var outcome = await command.Result.Task.WaitAsync(CommandTimeout, cancellationToken);
return TimetableResult(id, outcome, loop, ParseLocale(lang), classId: null, personId: null);
})
.WithName("PinSchoolLesson");
schools.MapDelete("/{id:int}/timetable/pin", async (
int id,
string? classId,
string? subject,
int? day,
int? period,
string? lang,
GameCommandQueue commands,
GameLoopService loop,
CancellationToken cancellationToken) =>
{
if (!TryDefName(classId, "classId", out var classValue, out var error)
|| !TryDefName(subject, "subject", out var subjectValue, out error)
|| day is null
|| period is null)
{
return Problem(
StatusCodes.Status400BadRequest,
"invalid-query",
error.Length > 0 ? error : "classId, subject, day and period are required.");
}
var command = new GameCommand.UnpinLesson(
id,
classValue,
subjectValue,
day.Value,
period.Value,
NewCompletion<TimetableOutcome>());
commands.Enqueue(command);
var outcome = await command.Result.Task.WaitAsync(CommandTimeout, cancellationToken);
return TimetableResult(id, outcome, loop, ParseLocale(lang), classId: null, personId: null);
})
.WithName("UnpinSchoolLesson");
}
private static TaskCompletionSource<T> NewCompletion<T>() =>
new(TaskCreationOptions.RunContinuationsAsynchronously);
private static string ParseLocale(string? lang) =>
string.Equals(lang, "en", StringComparison.OrdinalIgnoreCase) ? "en" : "ru";
private static TimetableResponse MapTimetable(
PublishedSchoolPeople published,
int weekDays,
string locale,
string? classId,
string? personId)
{
var table = published.Timetable ?? new Timetable([], []);
var roster = published.Roster ?? new Roster([], [], []);
if (published.Catalog is null)
{
return new TimetableResponse(weekDays, 0, [], [], [], []);
}
return TimetableMapper.From(
table,
roster,
published.Catalog,
published.Map,
weekDays,
locale,
classId,
personId);
}
private static IResult TimetableResult(
int schoolId,
TimetableOutcome outcome,
GameLoopService loop,
string locale,
string? classId,
string? personId)
{
if (outcome.Error != TimetableError.None)
{
return TimetableProblem(outcome.Error);
}
var published = loop.FindPeople(schoolId);
if (published is null)
{
return Problem(StatusCodes.Status404NotFound, "unknown-school", "That school does not exist.");
}
return Results.Ok(MapTimetable(published, loop.Options.SchoolWeekDays, locale, classId, personId));
}
private static IResult TimetableProblem(TimetableError error) =>
error switch
{
TimetableError.UnknownClass =>
Problem(StatusCodes.Status400BadRequest, "unknown-class", "That class is not in the school."),
TimetableError.UnknownSubject =>
Problem(StatusCodes.Status400BadRequest, "unknown-subject", "That subject is not in the catalog."),
TimetableError.UnknownRoom =>
Problem(StatusCodes.Status400BadRequest, "unknown-room", "That room is not on the map."),
TimetableError.NoTeacher =>
Problem(StatusCodes.Status409Conflict, "no-teacher", "Nobody is assigned that subject."),
TimetableError.PinRejected =>
Problem(StatusCodes.Status409Conflict, "pin-rejected", "That slot or room violates the timetable constraints."),
TimetableError.UnknownLesson =>
Problem(StatusCodes.Status404NotFound, "unknown-lesson", "That locked lesson is not on the timetable."),
_ => Problem(StatusCodes.Status404NotFound, "unknown-school", "That school does not exist."),
};
private static bool TryDefName(string? value, string field, out string name, out string error)
{
name = value?.Trim() ?? string.Empty;
if (name.Length is < 1 or > 64)
{
error = $"{field} is not valid.";
return false;
}
error = string.Empty;
return true;
}
private static IResult Problem(int statusCode, string code, string detail)
{
return Results.Problem(
detail: detail,
statusCode: statusCode,
title: code,
extensions: new Dictionary<string, object?> { ["code"] = code });
}
}