using System.Text.Json; using Server.WatchBody.Core.Dal.Src.Repository; using Server.WatchBody.Core.Programs; namespace Server.WatchBody.Web.Services; /// /// Сводка план/факт по активной версии (spec 05 §2 m08): недели от /// понедельника недели создания версии; performed — тренировки в /// плановые дни недели; missed = planned − performed. /// 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() }; var weeks = ParseWeeks(active.Body); var versionMonday = MondayOf(DateOnly.FromDateTime(active.CreatedAt)); var rows = new List(); 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 }; } /// Дни недели сессий по неделям body (v1 — каждая сессия /// = одна плановая тренировка; ротация недель — v2). private static List> ParseWeeks(string body) { var result = new List>(); try { using var doc = JsonDocument.Parse(body); foreach (var week in doc.RootElement.GetProperty("weeks").EnumerateArray()) { var days = new List(); 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]; }