Skip to content

Commit

Permalink
[600] feat: calculate how many minutes you will need per day/week in …
Browse files Browse the repository at this point in the history
…order to meet the goal by the end of the year (#601)
  • Loading branch information
philosowaffle authored Jan 7, 2024
1 parent 1297b86 commit 1a81b39
Show file tree
Hide file tree
Showing 10 changed files with 118 additions and 40 deletions.
18 changes: 16 additions & 2 deletions src/Api.Contract/PelotonAnnualChallengeContracts.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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; }
/// <summary>
/// Assuming working evenly throughout the whole year, this is the amount of time to plan to spend per day.
/// </summary>
public double MinutesNeededPerDay { get; set; }
/// <summary>
/// Assuming working evenly throughout the whole year, this is the amount of time to plan to spend per week.
/// </summary>
public double MinutesNeededPerWeek { get; set; }
/// <summary>
/// Assuming working evenly throughout the remainder of the year, this is the amount of time to plan to spend per day.
/// </summary>
public double MinutesNeededPerDayToFinishOnTime { get; set; }
/// <summary>
/// Assuming working evenly throughout the remainder of the year, this is the amount of time to plan to spend per week.
/// </summary>
public double MinutesNeededPerWeekToFinishOnTime { get; set; }
}
3 changes: 2 additions & 1 deletion src/Api.Service/ApiStartupServices.cs
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,8 @@ public static void ConfigureP2GApiServices(this IServiceCollection services)

// PELOTON
services.AddSingleton<IPelotonApi, Peloton.ApiClient>();
services.AddSingleton<IPelotonService, PelotonService>();
services.AddSingleton<IPelotonService, PelotonService>();
services.AddSingleton<IPelotonAnnualChallengeService, PelotonAnnualChallengeService>();
services.AddSingleton<IAnnualChallengeService, AnnualChallengeService>();

// RELEASE CHECKS
Expand Down
2 changes: 2 additions & 0 deletions src/Api.Service/Mappers/AnnualChallengeMapper.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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
};
}
}
57 changes: 57 additions & 0 deletions src/Api.Service/PelotonAnnualChallengeService.cs
Original file line number Diff line number Diff line change
@@ -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<ServiceResult<ProgressGetResponse>> GetProgressAsync();
}

public class PelotonAnnualChallengeService : IPelotonAnnualChallengeService
{
private readonly IAnnualChallengeService _service;

public PelotonAnnualChallengeService(IAnnualChallengeService service)
{
_service = service;
}

public async Task<ServiceResult<ProgressGetResponse>> GetProgressAsync()
{
var userId = 1;
var result = new ServiceResult<ProgressGetResponse>();

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<Contract.Tier>(),
};

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;
}
}
}
19 changes: 5 additions & 14 deletions src/Api/Controllers/PelotonAnnualChallengeController.cs
Original file line number Diff line number Diff line change
@@ -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;

Expand All @@ -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;
}
Expand All @@ -31,22 +30,14 @@ public PelotonAnnualChallengeController(IAnnualChallengeService annualChallengeS
[ProducesResponseType(typeof(ErrorResponse), StatusCodes.Status500InternalServerError)]
public async Task<ActionResult<ProgressGetResponse>> 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<Contract.Tier>(),
});
return Ok(serviceResult.Result);
}
catch (Exception e)
{
Expand Down
25 changes: 7 additions & 18 deletions src/ClientUI/ServiceClient.cs
Original file line number Diff line number Diff line change
@@ -1,18 +1,15 @@
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;
using Common.Service;
using Flurl.Http;
using Garmin.Auth;
using Peloton;
using Peloton.AnnualChallenge;
using Peloton.Dto;
using SharedUI;
using Sync;
Expand All @@ -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;
Expand All @@ -44,26 +41,18 @@ public ServiceClient(ISystemInfoService systemInfoService, ISettingsService sett

public async Task<ProgressGetResponse> 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<Api.Contract.Tier>(),
};
return result.Result;
}
catch (Exception e)
{
throw new ApiClientException($"Unexpected error ocurred: {e.Message}", e);
throw new ApiClientException($"Unexpected error occurred: {e.Message}", e);
}
}

Expand Down
15 changes: 15 additions & 0 deletions src/Peloton/AnnualChallenge/AnnualChallengeProgress.cs
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,21 @@ public record Tier
public bool IsOnTrackToEarndByEndOfYear { get; set; }
public double MinutesBehindPace { get; set; }
public double MinutesAheadOfPace { get; set; }

/// <summary>
/// Assuming working evenly throughout the whole year, this is the amount of time to plan to spend per day.
/// </summary>
public double MinutesNeededPerDay { get; set; }
/// <summary>
/// Assuming working evenly throughout the whole year, this is the amount of time to plan to spend per week.
/// </summary>
public double MinutesNeededPerWeek { get; set; }
/// <summary>
/// Assuming working evenly throughout the remainder of the year, this is the amount of time to plan to spend per day.
/// </summary>
public double MinutesNeededPerDayToFinishOnTime { get; set; }
/// <summary>
/// Assuming working evenly throughout the remainder of the year, this is the amount of time to plan to spend per week.
/// </summary>
public double MinutesNeededPerWeekToFinishOnTime { get; set; }
}
7 changes: 7 additions & 0 deletions src/Peloton/AnnualChallenge/AnnualChallengeService.cs
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,8 @@ public async Task<ServiceResult<AnnualChallengeProgress>> GetAnnualChallengeProg
MinutesAheadOfPace = onTrackDetails.MinutesBehindPace * -1,
MinutesNeededPerDay = onTrackDetails.MinutesNeededPerDay,
MinutesNeededPerWeek = onTrackDetails.MinutesNeededPerDay * 7,
MinutesNeededPerDayToFinishOnTime = onTrackDetails.MinutesNeededPerDayToFinishOnTime,
MinutesNeededPerWeekToFinishOnTime = onTrackDetails.MinutesNeededPerDayToFinishOnTime * 7
};
}).ToList();

Expand All @@ -83,13 +85,17 @@ 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,
MinutesBehindPace = neededMinutesToBeOnTrack - earnedMinutes,
MinutesNeededPerDay = minutesNeededPerDay,
HasEarned = earnedMinutes >= requiredMinutes,
PercentComplete = earnedMinutes / requiredMinutes,
MinutesNeededPerDayToFinishOnTime = minutesNeededPerDayToFinishOnTime
};
}

Expand All @@ -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; }
}
}
11 changes: 6 additions & 5 deletions src/SharedUI/Pages/AnnualChallengeProgress.razor
Original file line number Diff line number Diff line change
Expand Up @@ -19,11 +19,12 @@
@if (!tier.HasEarned)
{
<ul style="list-style: none; padding-left: 0;">
<li>Badge requires averaging <b>@Math.Round(tier.MinutesNeededPerDay, 2)</b> min/day or <b>@Math.Round(tier.MinutesNeededPerWeek, 2)</b> min/week.</li>
@if (!tier.IsOnTrackToEarndByEndOfYear)
{
<li>You are <b>@Math.Round(tier.MinutesBehindPace, 2)</b> minutes behind pace.</li>
}
<li>Badge requires averaging <b>@Math.Round(tier.MinutesNeededPerDay, 2)</b> min/day or <b>@Math.Round(tier.MinutesNeededPerWeek, 2)</b> min/week.</li>
@if (!tier.IsOnTrackToEarndByEndOfYear)
{
<li>You are <b>@Math.Round(tier.MinutesBehindPace, 2)</b> minutes behind pace.</li>
}
<li>You need <b>@Math.Round(tier.MinutesNeededPerDayToFinishOnTime, 2)</b> min/day or <b>@Math.Round(tier.MinutesNeededPerWeekToFinishOnTime, 2)</b> min/week in order to finish in time.</li>
</ul>
}

Expand Down
1 change: 1 addition & 0 deletions vNextReleaseNotes.md
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down

0 comments on commit 1a81b39

Please sign in to comment.