- 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.
190 lines
6.9 KiB
C#
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 });
|
|
}
|
|
}
|