Enhance staffing management with uncovered teacher tracking
ci / server (push) Failing after 3m43s
ci / client (push) Successful in 14s

- Updated `protocol.md` to clarify the concept of uncovered subjects and the number of additional teachers required.
- Introduced `teachersShort` property in the `StaffingSubject` interface to indicate how many more teachers are needed for a subject.
- Enhanced localization strings to reflect the new uncovered teacher information in both English and Russian.
- Updated the management panel UI to display the number of teachers short for each uncovered subject.
- Revised the `Uncovered` method in the staffing logic to return detailed information about uncovered subjects, including the shortfall of teachers.
- Added tests to validate the new uncovered teacher tracking functionality and ensure accurate reporting in the staffing API.
- Marked related tasks as complete in the documentation for the golden fixtures phase.
This commit is contained in:
Leonid Pershin
2026-08-19 23:53:37 +03:00
parent c27d43f66a
commit e5a0ae5b9d
27 changed files with 16055 additions and 38 deletions
@@ -0,0 +1,103 @@
using System.Text.Encodings.Web;
using System.Text.Json;
using System.Text.Json.Nodes;
using HSchool.Content;
namespace HSchool.People.Tests;
/// <summary>
/// Writes the host golden save files. Not a correctness test — run with WRITE_GOLDEN=1 after an
/// intentional generation change, then commit the AppHost fixtures in the same change.
/// </summary>
public class GoldenSaveDump
{
[Fact]
public void Dump_HostSaveFixtures_WhenAsked()
{
if (!string.Equals(Environment.GetEnvironmentVariable("WRITE_GOLDEN"), "1", StringComparison.Ordinal))
{
return;
}
var catalog = Fixtures.Catalog();
var map = TwoClassrooms();
var roster = RosterGenerator.Generate(catalog, map, schoolSeed: 1, "Slavic", Fixtures.AsOf, "RussianLanguage");
var pool = ApplicantPool.Create(catalog, roster, schoolSeed: 1, "Slavic", Fixtures.AsOf, "RussianLanguage");
var people = RosterJson.Serialize(RosterDocument.From(1, roster, pool));
var mapNode = JsonNode.Parse(JsonSerializer.Serialize(map, Jsonc.SerializerOptions));
var current = SchoolJson(mapNode, includeNative: true);
var legacy = SchoolJson(mapNode, includeNative: false);
var root = Path.Combine(Fixtures.RepoRoot(), "tests", "HSchool.AppHost.Tests", "golden");
Write(Path.Combine(root, "current", "1.json"), current);
Write(Path.Combine(root, "current", "1.people.json"), people);
Write(Path.Combine(root, "legacy-no-native", "1.json"), legacy);
Write(Path.Combine(root, "legacy-no-native", "1.people.json"), people);
}
internal static MapLayout TwoClassrooms() =>
new()
{
Territory = new TerritoryNode { Id = "yard", Def = "SchoolYard" },
Buildings = [new BuildingNode { Id = "main", Def = "MainBuilding" }],
Floors = [new FloorNode { Id = "floor-1", Def = "StandardFloor", Building = "main", Label = "1" }],
Rooms =
[
new RoomNode
{
Id = "classroom-00",
Def = "Classroom",
Building = "main",
Floor = "floor-1",
Label = "101",
Seats = 16,
},
new RoomNode
{
Id = "classroom-01",
Def = "Classroom",
Building = "main",
Floor = "floor-1",
Label = "102",
Seats = 16,
},
],
Links =
[
new MapLink { A = "yard", B = "classroom-00" },
new MapLink { A = "yard", B = "classroom-01" },
],
};
private static string SchoolJson(JsonNode? map, bool includeNative)
{
var root = new JsonObject
{
["format"] = 2,
["id"] = 1,
["name"] = "Золотая",
["gameTime"] = "2012-03-31T06:00:00Z",
["running"] = false,
["speedIndex"] = 1,
["modIds"] = new JsonArray("core"),
["map"] = map?.DeepClone(),
["nameSetId"] = "Slavic",
};
if (includeNative)
{
root["nativeLanguage"] = "RussianLanguage";
}
return root.ToJsonString(new JsonSerializerOptions
{
WriteIndented = true,
Encoder = JavaScriptEncoder.UnsafeRelaxedJsonEscaping,
});
}
private static void Write(string path, string contents)
{
Directory.CreateDirectory(Path.GetDirectoryName(path)!);
File.WriteAllText(path, contents);
}
}