98 lines
4.0 KiB
C#
98 lines
4.0 KiB
C#
using System.Text.Json;
|
||
using Server.WatchBody.Core.Dal.Src.Repository;
|
||
using Server.WatchBody.Core.Programs;
|
||
|
||
namespace Server.WatchBody.Web.Services;
|
||
|
||
/// <summary>
|
||
/// Сводка план/факт по активной версии (spec 05 §2 m08): недели от
|
||
/// понедельника недели создания версии; performed — тренировки в
|
||
/// плановые дни недели; missed = planned − performed.
|
||
/// </summary>
|
||
public class PlanSummaryService(
|
||
IWorkoutRepository workouts,
|
||
IWorkoutMetricsRepository metrics,
|
||
ProgramStore programs)
|
||
{
|
||
private static readonly string[] DayNames =
|
||
["monday", "tuesday", "wednesday", "thursday", "friday", "saturday", "sunday"];
|
||
|
||
public object Summarize(int athleteId)
|
||
{
|
||
var active = programs.GetActive(athleteId);
|
||
if (active == null || string.IsNullOrWhiteSpace(active.Body))
|
||
return new { activeVersion = (int?)null, weeks = Array.Empty<object>() };
|
||
|
||
var weeks = ParseWeeks(active.Body);
|
||
var versionMonday = MondayOf(DateOnly.FromDateTime(active.CreatedAt));
|
||
|
||
var rows = new List<object>();
|
||
for (var i = 0; i < weeks.Count; i++)
|
||
{
|
||
var from = versionMonday.AddDays(7 * i);
|
||
var to = from.AddDays(6);
|
||
var plannedDays = weeks[i].ToHashSet(StringComparer.OrdinalIgnoreCase);
|
||
var planned = weeks[i].Count;
|
||
if (planned == 0) continue;
|
||
|
||
var inWeek = workouts.Select(new WorkoutFilter
|
||
{
|
||
AthleteId = athleteId,
|
||
DateFrom = from,
|
||
DateTo = to,
|
||
}).ToList();
|
||
// performed — только в плановые дни недели (spec 05)
|
||
var performedList = inWeek
|
||
.Where(w => plannedDays.Contains(DayName(DateOnly.FromDateTime(w.StartedAt))))
|
||
.ToList();
|
||
var adherences = performedList
|
||
.Select(w => metrics.Select(new WorkoutMetricsFilter
|
||
{ AthleteId = athleteId, WorkoutId = w.Id }).FirstOrDefault()?.Adherence)
|
||
.Where(a => a != null).ToList();
|
||
rows.Add(new
|
||
{
|
||
week = i + 1,
|
||
from = from.ToString("yyyy-MM-dd"),
|
||
to = to.ToString("yyyy-MM-dd"),
|
||
planned,
|
||
performed = performedList.Count,
|
||
missed = Math.Max(0, planned - performedList.Count),
|
||
avgAdherence = adherences.Count > 0
|
||
? Math.Round(adherences.Average(a => a!.Value), 1, MidpointRounding.AwayFromZero)
|
||
: (decimal?)null,
|
||
});
|
||
}
|
||
return new { activeVersion = (int?)active.Version, weeks = rows };
|
||
}
|
||
|
||
/// <summary>Дни недели сессий по неделям body (v1 — каждая сессия
|
||
/// = одна плановая тренировка; ротация недель — v2).</summary>
|
||
private static List<List<string>> ParseWeeks(string body)
|
||
{
|
||
var result = new List<List<string>>();
|
||
try
|
||
{
|
||
using var doc = JsonDocument.Parse(body);
|
||
foreach (var week in doc.RootElement.GetProperty("weeks").EnumerateArray())
|
||
{
|
||
var days = new List<string>();
|
||
if (week.TryGetProperty("sessions", out var sessionNodes))
|
||
foreach (var session in sessionNodes.EnumerateArray())
|
||
if (session.TryGetProperty("day", out var day) && day.ValueKind == JsonValueKind.String)
|
||
days.Add(day.GetString()!);
|
||
result.Add(days);
|
||
}
|
||
}
|
||
catch (Exception ex) when (ex is JsonException or KeyNotFoundException)
|
||
{
|
||
// кривой body — сводка пустая
|
||
}
|
||
return result;
|
||
}
|
||
|
||
private static DateOnly MondayOf(DateOnly date) =>
|
||
date.AddDays(-(((int)date.DayOfWeek + 6) % 7));
|
||
|
||
private static string DayName(DateOnly date) => DayNames[((int)date.DayOfWeek + 6) % 7];
|
||
}
|