Add lesson attendance from presence (present/late/absent).
Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -0,0 +1,154 @@
|
||||
using System.Text.Json.Serialization;
|
||||
using HSchool.Content;
|
||||
|
||||
namespace HSchool.People;
|
||||
|
||||
/// <summary>Stable status ids written into <see cref="AttendanceRecord.Status"/>.</summary>
|
||||
public static class AttendanceStatuses
|
||||
{
|
||||
public const string Present = "present";
|
||||
|
||||
public const string Late = "late";
|
||||
|
||||
public const string Absent = "absent";
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Why a pupil was absent. Vanilla writes <see cref="Truancy"/>; slice 14 fills illness.
|
||||
/// </summary>
|
||||
public static class AbsenceReasons
|
||||
{
|
||||
public const string Truancy = "truancy";
|
||||
}
|
||||
|
||||
/// <summary>One lesson-slot attendance row. Sparse list on the person — not a year journal.</summary>
|
||||
public sealed class AttendanceRecord
|
||||
{
|
||||
public required string Subject { get; init; }
|
||||
|
||||
/// <summary><see cref="AttendanceStatuses"/> id.</summary>
|
||||
public required string Status { get; init; }
|
||||
|
||||
public required DateTime Time { get; init; }
|
||||
|
||||
public required int Period { get; init; }
|
||||
|
||||
/// <summary>Set when <see cref="Status"/> is absent. Omitted for present/late.</summary>
|
||||
[JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]
|
||||
public string? AbsenceReason { get; init; }
|
||||
}
|
||||
|
||||
/// <summary>Appends or upgrades recent attendance and drops the oldest when over the BehaviorDef ceiling.</summary>
|
||||
public static class AttendanceMemory
|
||||
{
|
||||
public static AttendanceRecord? Find(Person person, DateTime day, int period)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(person);
|
||||
var list = person.Attendance;
|
||||
if (list is null || list.Count == 0)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
var date = DateTime.SpecifyKind(day, DateTimeKind.Utc).Date;
|
||||
for (var i = list.Count - 1; i >= 0; i--)
|
||||
{
|
||||
var row = list[i];
|
||||
if (row.Period == period && row.Time.Date == date)
|
||||
{
|
||||
return row;
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Writes a new row, or upgrades an existing absent row to present/late. Present/late once
|
||||
/// claimed for the slot stay put — first arrival wins.
|
||||
/// </summary>
|
||||
public static bool Record(
|
||||
Person person,
|
||||
string subject,
|
||||
string status,
|
||||
DateTime time,
|
||||
int period,
|
||||
BehaviorDef rules,
|
||||
string? absenceReason = null)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(person);
|
||||
ArgumentNullException.ThrowIfNull(rules);
|
||||
ArgumentException.ThrowIfNullOrWhiteSpace(subject);
|
||||
ArgumentException.ThrowIfNullOrWhiteSpace(status);
|
||||
|
||||
if (rules.AttendanceMax <= 0 || !IsKnownStatus(status))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
var list = person.Attendance;
|
||||
if (list is null)
|
||||
{
|
||||
list = [];
|
||||
person.Attendance = list;
|
||||
}
|
||||
|
||||
var date = DateTime.SpecifyKind(time, DateTimeKind.Utc).Date;
|
||||
for (var i = list.Count - 1; i >= 0; i--)
|
||||
{
|
||||
var existing = list[i];
|
||||
if (existing.Period != period || existing.Time.Date != date)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
if (existing.Status is AttendanceStatuses.Present or AttendanceStatuses.Late)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
if (existing.Status == AttendanceStatuses.Absent
|
||||
&& status is AttendanceStatuses.Present or AttendanceStatuses.Late)
|
||||
{
|
||||
list[i] = Build(subject, status, time, period, absenceReason: null);
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
list.Add(Build(
|
||||
subject,
|
||||
status,
|
||||
time,
|
||||
period,
|
||||
status == AttendanceStatuses.Absent
|
||||
? (string.IsNullOrWhiteSpace(absenceReason) ? AbsenceReasons.Truancy : absenceReason)
|
||||
: null));
|
||||
|
||||
while (list.Count > rules.AttendanceMax)
|
||||
{
|
||||
list.RemoveAt(0);
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
private static AttendanceRecord Build(
|
||||
string subject,
|
||||
string status,
|
||||
DateTime time,
|
||||
int period,
|
||||
string? absenceReason) =>
|
||||
new()
|
||||
{
|
||||
Subject = subject,
|
||||
Status = status,
|
||||
Time = DateTime.SpecifyKind(time, DateTimeKind.Utc),
|
||||
Period = period,
|
||||
AbsenceReason = absenceReason,
|
||||
};
|
||||
|
||||
private static bool IsKnownStatus(string status) =>
|
||||
status is AttendanceStatuses.Present or AttendanceStatuses.Late or AttendanceStatuses.Absent;
|
||||
}
|
||||
@@ -81,6 +81,12 @@ public sealed record Person
|
||||
/// </summary>
|
||||
public List<LessonMarkRecord>? LessonMarks { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Recent lesson attendance (present / late / absent). Null when empty so people.json stays compact.
|
||||
/// Ceiling and late threshold live on <c>BehaviorDef</c>.
|
||||
/// </summary>
|
||||
public List<AttendanceRecord>? Attendance { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Active medical conditions. Null when healthy (empty) so people.json stays compact.
|
||||
/// Needs are not replaced by this list.
|
||||
|
||||
Reference in New Issue
Block a user