diff --git a/src/Api.Contract/PelotonAnnualChallengeContracts.cs b/src/Api.Contract/PelotonAnnualChallengeContracts.cs index 07bdc1c45..3b30db467 100644 --- a/src/Api.Contract/PelotonAnnualChallengeContracts.cs +++ b/src/Api.Contract/PelotonAnnualChallengeContracts.cs @@ -23,6 +23,20 @@ public Tier() { } public bool IsOnTrackToEarndByEndOfYear { get; init; } public double MinutesBehindPace { get; init; } public double MinutesAheadOfPace { get; init; } - public double MinutesNeededPerDay { get; init; } - public double MinutesNeededPerWeek { get; init; } + /// + /// Assuming working evenly throughout the whole year, this is the amount of time to plan to spend per day. + /// + public double MinutesNeededPerDay { get; set; } + /// + /// Assuming working evenly throughout the whole year, this is the amount of time to plan to spend per week. + /// + public double MinutesNeededPerWeek { get; set; } + /// + /// Assuming working evenly throughout the remainder of the year, this is the amount of time to plan to spend per day. + /// + public double MinutesNeededPerDayToFinishOnTime { get; set; } + /// + /// Assuming working evenly throughout the remainder of the year, this is the amount of time to plan to spend per week. + /// + public double MinutesNeededPerWeekToFinishOnTime { get; set; } } \ No newline at end of file diff --git a/src/Api.Service/ApiStartupServices.cs b/src/Api.Service/ApiStartupServices.cs index c090207f4..83b61fac3 100644 --- a/src/Api.Service/ApiStartupServices.cs +++ b/src/Api.Service/ApiStartupServices.cs @@ -40,7 +40,8 @@ public static void ConfigureP2GApiServices(this IServiceCollection services) // PELOTON services.AddSingleton(); - services.AddSingleton(); + services.AddSingleton(); + services.AddSingleton(); services.AddSingleton(); // RELEASE CHECKS diff --git a/src/Api.Service/Mappers/AnnualChallengeMapper.cs b/src/Api.Service/Mappers/AnnualChallengeMapper.cs index 9973ff220..6f5861d26 100644 --- a/src/Api.Service/Mappers/AnnualChallengeMapper.cs +++ b/src/Api.Service/Mappers/AnnualChallengeMapper.cs @@ -18,6 +18,8 @@ public static Tier Map(this Peloton.AnnualChallenge.Tier t) MinutesAheadOfPace = t.MinutesAheadOfPace, MinutesNeededPerDay = t.MinutesNeededPerDay, MinutesNeededPerWeek = t.MinutesNeededPerWeek, + MinutesNeededPerDayToFinishOnTime = t.MinutesNeededPerDayToFinishOnTime, + MinutesNeededPerWeekToFinishOnTime = t.MinutesNeededPerWeekToFinishOnTime }; } } diff --git a/src/Api.Service/PelotonAnnualChallengeService.cs b/src/Api.Service/PelotonAnnualChallengeService.cs new file mode 100644 index 000000000..88b95c047 --- /dev/null +++ b/src/Api.Service/PelotonAnnualChallengeService.cs @@ -0,0 +1,57 @@ +using Api.Contract; +using Api.Service.Helpers; +using Api.Service.Mappers; +using Common.Dto; +using Peloton.AnnualChallenge; + +namespace Api.Service; + +public interface IPelotonAnnualChallengeService +{ + Task> GetProgressAsync(); +} + +public class PelotonAnnualChallengeService : IPelotonAnnualChallengeService +{ + private readonly IAnnualChallengeService _service; + + public PelotonAnnualChallengeService(IAnnualChallengeService service) + { + _service = service; + } + + public async Task> GetProgressAsync() + { + var userId = 1; + var result = new ServiceResult(); + + try + { + var serviceResult = await _service.GetAnnualChallengeProgressAsync(userId); + + if (serviceResult.IsErrored()) + { + result.Successful = serviceResult.Successful; + result.Error = serviceResult.Error; + return result; + } + + var data = serviceResult.Result; + var tiers = data.Tiers?.Select(t => t.Map()).ToList(); + + result.Result = new ProgressGetResponse() + { + EarnedMinutes = data.EarnedMinutes, + Tiers = tiers ?? new List(), + }; + + return result; + } + catch (Exception e) + { + result.Successful = false; + result.Error = new ServiceError() { Exception = e, Message = "Failed to fetch Peloton Annual Challenge data. See logs for more details." }; + return result; + } + } +} diff --git a/src/Api/Controllers/PelotonAnnualChallengeController.cs b/src/Api/Controllers/PelotonAnnualChallengeController.cs index 6c6f19373..6d3ce31e8 100644 --- a/src/Api/Controllers/PelotonAnnualChallengeController.cs +++ b/src/Api/Controllers/PelotonAnnualChallengeController.cs @@ -1,8 +1,7 @@ using Api.Contract; +using Api.Service; using Api.Service.Helpers; -using Api.Service.Mappers; using Microsoft.AspNetCore.Mvc; -using Peloton.AnnualChallenge; namespace Api.Controllers; @@ -11,9 +10,9 @@ namespace Api.Controllers; [Consumes("application/json")] public class PelotonAnnualChallengeController : Controller { - private readonly IAnnualChallengeService _annualChallengeService; + private readonly IPelotonAnnualChallengeService _annualChallengeService; - public PelotonAnnualChallengeController(IAnnualChallengeService annualChallengeService) + public PelotonAnnualChallengeController(IPelotonAnnualChallengeService annualChallengeService) { _annualChallengeService = annualChallengeService; } @@ -31,22 +30,14 @@ public PelotonAnnualChallengeController(IAnnualChallengeService annualChallengeS [ProducesResponseType(typeof(ErrorResponse), StatusCodes.Status500InternalServerError)] public async Task> GetProgressSummaryAsync() { - var userId = 1; try { - var serviceResult = await _annualChallengeService.GetAnnualChallengeProgressAsync(userId); + var serviceResult = await _annualChallengeService.GetProgressAsync(); if (serviceResult.IsErrored()) return serviceResult.GetResultForError(); - var data = serviceResult.Result; - var tiers = data.Tiers?.Select(t => t.Map()).ToList(); - - return Ok(new ProgressGetResponse() - { - EarnedMinutes = data.EarnedMinutes, - Tiers = tiers ?? new List(), - }); + return Ok(serviceResult.Result); } catch (Exception e) { diff --git a/src/ClientUI/ServiceClient.cs b/src/ClientUI/ServiceClient.cs index a209cf9aa..301fffd17 100644 --- a/src/ClientUI/ServiceClient.cs +++ b/src/ClientUI/ServiceClient.cs @@ -1,10 +1,8 @@ using Api.Contract; using Api.Service; using Api.Service.Helpers; -using Api.Service.Mappers; using Api.Service.Validators; using Api.Services; -using Common; using Common.Database; using Common.Dto.Peloton; using Common.Dto; @@ -12,7 +10,6 @@ using Flurl.Http; using Garmin.Auth; using Peloton; -using Peloton.AnnualChallenge; using Peloton.Dto; using SharedUI; using Sync; @@ -24,13 +21,13 @@ public class ServiceClient : IApiClient private readonly ISystemInfoService _systemInfoService; private readonly ISettingsService _settingsService; private readonly ISettingsUpdaterService _settingsUpdaterService; - private readonly IAnnualChallengeService _annualChallengeService; + private readonly IPelotonAnnualChallengeService _annualChallengeService; private readonly IPelotonService _pelotonService; private readonly IGarminAuthenticationService _garminAuthService; private readonly ISyncService _syncService; private readonly ISyncStatusDb _syncStatusDb; - public ServiceClient(ISystemInfoService systemInfoService, ISettingsService settingsService, IAnnualChallengeService annualChallengeService, ISettingsUpdaterService settingsUpdaterService, IPelotonService pelotonService, IGarminAuthenticationService garminAuthService, ISyncService syncService, ISyncStatusDb syncStatusDb) + public ServiceClient(ISystemInfoService systemInfoService, ISettingsService settingsService, IPelotonAnnualChallengeService annualChallengeService, ISettingsUpdaterService settingsUpdaterService, IPelotonService pelotonService, IGarminAuthenticationService garminAuthService, ISyncService syncService, ISyncStatusDb syncStatusDb) { _systemInfoService = systemInfoService; _settingsService = settingsService; @@ -44,26 +41,18 @@ public ServiceClient(ISystemInfoService systemInfoService, ISettingsService sett public async Task GetAnnualProgressAsync() { - var userId = 1; try { - var serviceResult = await _annualChallengeService.GetAnnualChallengeProgressAsync(userId); + var result = await _annualChallengeService.GetProgressAsync(); - if (serviceResult.IsErrored()) - throw new ApiClientException(serviceResult.Error.Message, serviceResult.Error.Exception); - - var data = serviceResult.Result; - var tiers = data.Tiers?.Select(t => t.Map()).ToList(); + if (result.IsErrored()) + throw new ApiClientException(result.Error.Message, result.Error.Exception); - return new ProgressGetResponse() - { - EarnedMinutes = data.EarnedMinutes, - Tiers = tiers ?? new List(), - }; + return result.Result; } catch (Exception e) { - throw new ApiClientException($"Unexpected error ocurred: {e.Message}", e); + throw new ApiClientException($"Unexpected error occurred: {e.Message}", e); } } diff --git a/src/Peloton/AnnualChallenge/AnnualChallengeProgress.cs b/src/Peloton/AnnualChallenge/AnnualChallengeProgress.cs index 4411e60b6..bbf709555 100644 --- a/src/Peloton/AnnualChallenge/AnnualChallengeProgress.cs +++ b/src/Peloton/AnnualChallenge/AnnualChallengeProgress.cs @@ -19,6 +19,21 @@ public record Tier public bool IsOnTrackToEarndByEndOfYear { get; set; } public double MinutesBehindPace { get; set; } public double MinutesAheadOfPace { get; set; } + + /// + /// Assuming working evenly throughout the whole year, this is the amount of time to plan to spend per day. + /// public double MinutesNeededPerDay { get; set; } + /// + /// Assuming working evenly throughout the whole year, this is the amount of time to plan to spend per week. + /// public double MinutesNeededPerWeek { get; set; } + /// + /// Assuming working evenly throughout the remainder of the year, this is the amount of time to plan to spend per day. + /// + public double MinutesNeededPerDayToFinishOnTime { get; set; } + /// + /// Assuming working evenly throughout the remainder of the year, this is the amount of time to plan to spend per week. + /// + public double MinutesNeededPerWeekToFinishOnTime { get; set; } } diff --git a/src/Peloton/AnnualChallenge/AnnualChallengeService.cs b/src/Peloton/AnnualChallenge/AnnualChallengeService.cs index 162f6719e..95bb33f84 100644 --- a/src/Peloton/AnnualChallenge/AnnualChallengeService.cs +++ b/src/Peloton/AnnualChallenge/AnnualChallengeService.cs @@ -65,6 +65,8 @@ public async Task> GetAnnualChallengeProg MinutesAheadOfPace = onTrackDetails.MinutesBehindPace * -1, MinutesNeededPerDay = onTrackDetails.MinutesNeededPerDay, MinutesNeededPerWeek = onTrackDetails.MinutesNeededPerDay * 7, + MinutesNeededPerDayToFinishOnTime = onTrackDetails.MinutesNeededPerDayToFinishOnTime, + MinutesNeededPerWeekToFinishOnTime = onTrackDetails.MinutesNeededPerDayToFinishOnTime * 7 }; }).ToList(); @@ -83,6 +85,9 @@ public static OnTrackDetails CalculateOnTrackDetails(DateTime now, DateTime star var neededMinutesToBeOnTrack = elapsedDays * minutesNeededPerDay; + var remainingDays = Math.Ceiling((endTimeUtc - now).TotalDays); + var minutesNeededPerDayToFinishOnTime = (requiredMinutes - earnedMinutes) / remainingDays; + return new OnTrackDetails() { IsOnTrackToEarnByEndOfYear = earnedMinutes >= neededMinutesToBeOnTrack, @@ -90,6 +95,7 @@ public static OnTrackDetails CalculateOnTrackDetails(DateTime now, DateTime star MinutesNeededPerDay = minutesNeededPerDay, HasEarned = earnedMinutes >= requiredMinutes, PercentComplete = earnedMinutes / requiredMinutes, + MinutesNeededPerDayToFinishOnTime = minutesNeededPerDayToFinishOnTime }; } @@ -100,5 +106,6 @@ public record OnTrackDetails public double MinutesNeededPerDay { get; init; } public bool HasEarned { get; init; } public double PercentComplete { get; init; } + public double MinutesNeededPerDayToFinishOnTime { get; init; } } } diff --git a/src/SharedUI/Pages/AnnualChallengeProgress.razor b/src/SharedUI/Pages/AnnualChallengeProgress.razor index 0478b0c37..777388023 100644 --- a/src/SharedUI/Pages/AnnualChallengeProgress.razor +++ b/src/SharedUI/Pages/AnnualChallengeProgress.razor @@ -19,11 +19,12 @@ @if (!tier.HasEarned) {
    -
  • Badge requires averaging @Math.Round(tier.MinutesNeededPerDay, 2) min/day or @Math.Round(tier.MinutesNeededPerWeek, 2) min/week.
  • - @if (!tier.IsOnTrackToEarndByEndOfYear) - { -
  • You are @Math.Round(tier.MinutesBehindPace, 2) minutes behind pace.
  • - } +
  • Badge requires averaging @Math.Round(tier.MinutesNeededPerDay, 2) min/day or @Math.Round(tier.MinutesNeededPerWeek, 2) min/week.
  • + @if (!tier.IsOnTrackToEarndByEndOfYear) + { +
  • You are @Math.Round(tier.MinutesBehindPace, 2) minutes behind pace.
  • + } +
  • You need @Math.Round(tier.MinutesNeededPerDayToFinishOnTime, 2) min/day or @Math.Round(tier.MinutesNeededPerWeekToFinishOnTime, 2) min/week in order to finish in time.
} diff --git a/vNextReleaseNotes.md b/vNextReleaseNotes.md index 82208caa6..daf07c39e 100644 --- a/vNextReleaseNotes.md +++ b/vNextReleaseNotes.md @@ -5,6 +5,7 @@ - [#564] [#591] Set a custom title on Workouts using templating - [#559] Ability to exclude Outdoor Cycling workouts from sycning +- [#600] Annual Challenge page now let's you know how many minutes you will need per day/week in order to meet the goal by the end of the year (based on the remaining time left) ## Fixes