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