Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add timezone on clockify entry fill #116

Open
wants to merge 8 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 9 additions & 4 deletions src/Clockify/Fill/EntryFillDialog.cs
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
using System.Threading.Tasks;
using Bot.Clockify.Client;
using Bot.Clockify.Models;
using Bot.Common;
using Bot.Common.ChannelData.Telegram;
using Bot.Common.Recognizer;
using Bot.Data;
Expand All @@ -26,6 +27,7 @@ public class EntryFillDialog : ComponentDialog
private readonly WorthAskingForTaskService _worthAskingForTask;
private readonly UserState _userState;
private readonly IClockifyMessageSource _messageSource;
private readonly IDateTimeProvider _dateTimeProvider;
private readonly ILogger<EntryFillDialog> _logger;

private const string TaskWaterfall = "TaskWaterfall";
Expand All @@ -41,7 +43,7 @@ public class EntryFillDialog : ComponentDialog
public EntryFillDialog(ClockifyEntityRecognizer clockifyWorkableRecognizer,
ITimeEntryStoreService timeEntryStoreService, WorthAskingForTaskService worthAskingForTask,
UserState userState, IClockifyService clockifyService, ITokenRepository tokenRepository,
IClockifyMessageSource messageSource, ILogger<EntryFillDialog> logger)
IClockifyMessageSource messageSource, IDateTimeProvider dateTimeProvider, ILogger<EntryFillDialog> logger)
{
_clockifyWorkableRecognizer = clockifyWorkableRecognizer;
_timeEntryStoreService = timeEntryStoreService;
Expand All @@ -50,6 +52,7 @@ public EntryFillDialog(ClockifyEntityRecognizer clockifyWorkableRecognizer,
_clockifyService = clockifyService;
_tokenRepository = tokenRepository;
_messageSource = messageSource;
_dateTimeProvider = dateTimeProvider;
_logger = logger;
AddDialog(new WaterfallDialog(TaskWaterfall, new List<WaterfallStep>
{
Expand Down Expand Up @@ -77,8 +80,9 @@ private async Task<DialogTurnResult> PromptForTaskAsync(WaterfallStepContext ste
var recognizedProject =
await _clockifyWorkableRecognizer.RecognizeProject(luisResult.ProjectName(), clockifyToken);
stepContext.Values["Project"] = recognizedProject;
stepContext.Values["TimeZone"] = userProfile.TimeZone;
double minutes = luisResult.WorkedDurationInMinutes();
var (start, end) = luisResult.WorkedPeriod(minutes);
var (start, end) = luisResult.WorkedPeriod(_dateTimeProvider, minutes, userProfile.TimeZone);
stepContext.Values["Start"] = start;
stepContext.Values["End"] = end;
string fullEntity = recognizedProject.Name;
Expand Down Expand Up @@ -247,12 +251,13 @@ private async Task<bool> ClockifyTaskValidatorAsync(PromptValidatorContext<strin
}
}

private async Task<DialogTurnResult> AddEntryAndExit(DialogContext stepContext,
private async Task<DialogTurnResult> AddEntryAndExit(WaterfallStepContext stepContext,
khaelys marked this conversation as resolved.
Show resolved Hide resolved
CancellationToken cancellationToken, string clockifyToken, ProjectDo recognizedProject,
DateTime start, DateTime end, string fullEntity, TaskDo? task)
{
var timeZone = (TimeZoneInfo) stepContext.Values["TimeZone"];
double current =
await _timeEntryStoreService.AddTimeEntries(clockifyToken, recognizedProject, task, start, end);
await _timeEntryStoreService.AddTimeEntries(clockifyToken, recognizedProject, task, start, end, timeZone);
string messageText =
string.Format(_messageSource.AddEntryFeedback, (end-start).TotalMinutes, fullEntity, current);
string platform = stepContext.Context.Activity.ChannelId;
Expand Down
3 changes: 2 additions & 1 deletion src/Clockify/Fill/ITimeEntryStoreService.cs
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ namespace Bot.Clockify.Fill
{
public interface ITimeEntryStoreService
{
public Task<double> AddTimeEntries(string clockifyToken, ProjectDo project, TaskDo? task, DateTime start, DateTime end);
public Task<double> AddTimeEntries(string clockifyToken, ProjectDo project, TaskDo? task, DateTime start,
DateTime end, TimeZoneInfo timeZone);
}
}
15 changes: 11 additions & 4 deletions src/Clockify/Fill/TimeEntryStoreService.cs
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
using System.Threading.Tasks;
using Bot.Clockify.Client;
using Bot.Clockify.Models;
using Bot.Common;
using Microsoft.Extensions.Configuration;

namespace Bot.Clockify.Fill
Expand All @@ -12,14 +13,18 @@ public class TimeEntryStoreService : ITimeEntryStoreService
{
private readonly IClockifyService _clockifyService;
private readonly string _tagName;
private readonly IDateTimeProvider _dateTimeProvider;

public TimeEntryStoreService(IClockifyService clockifyService, IConfiguration configuration)
public TimeEntryStoreService(IClockifyService clockifyService, IConfiguration configuration,
IDateTimeProvider dateTimeProvider)
{
_clockifyService = clockifyService;
_dateTimeProvider = dateTimeProvider;
_tagName = configuration["Tag"];
}

public async Task<double> AddTimeEntries(string clockifyToken, ProjectDo project, TaskDo? task, DateTime start, DateTime end)
public async Task<double> AddTimeEntries(string clockifyToken, ProjectDo project, TaskDo? task, DateTime start,
DateTime end, TimeZoneInfo timeZone)
{
string? tagId = await _clockifyService.GetTagAsync(clockifyToken, project.WorkspaceId, _tagName);
string userId = (await _clockifyService.GetCurrentUserAsync(clockifyToken)).Id;
Expand All @@ -31,13 +36,15 @@ public async Task<double> AddTimeEntries(string clockifyToken, ProjectDo project
taskId: task?.Id,
billable: project.Billable,
end: end,
tagIds: (tagId != null) ? new List<string> {tagId} : null
tagIds: tagId != null ? new List<string> { tagId } : null
);

await _clockifyService.AddTimeEntryAsync(clockifyToken, workspaceId, timeEntry);

// TODO Extract total hours calculation into another method
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can we solve it directly in this pr?

var userToday = TimeZoneInfo.ConvertTime(_dateTimeProvider.DateTimeUtcNow(), timeZone).Date;
var todayEntries = await _clockifyService.GetHydratedTimeEntriesAsync(clockifyToken, workspaceId, userId,
new DateTimeOffset(DateTime.Today), new DateTimeOffset(DateTime.Today.AddDays(1)));
new DateTimeOffset(userToday), new DateTimeOffset(userToday.AddDays(1)));

return todayEntries
.Where(entry => entry.TimeInterval.Start.HasValue && entry.TimeInterval.End.HasValue)
Expand Down
78 changes: 73 additions & 5 deletions src/Common/Recognizer/TimeSurveyBotLuisEx.cs
Original file line number Diff line number Diff line change
Expand Up @@ -69,18 +69,86 @@ ex is InvalidOperationException
}
}

public (DateTime start, DateTime end) WorkedPeriod(double minutes, string culture = Culture.English)
public (DateTime start, DateTime end) WorkedPeriod(IDateTimeProvider dateTimeProvider, double minutes, TimeZoneInfo timeZone,
string culture = Culture.English)
{
var workedPeriodInstances = Entities._instance.datetime;
var userNow = TimeZoneInfo.ConvertTime(dateTimeProvider.DateTimeUtcNow(), timeZone);
if (workedPeriodInstances.Length > 1)
{
string? instance = workedPeriodInstances[1].Text;
var recognizedDateTime = DateTimeRecognizer.RecognizeDateTime(instance, culture).First();
var recognizedDateTime =
DateTimeRecognizer.RecognizeDateTime(instance, culture, refTime: userNow).First();
var resolvedPeriod = ((List<Dictionary<string, string>>)recognizedDateTime.Resolution["values"])[0];
// TODO: use resolvedPeriod to pick a (start, end) period
khaelys marked this conversation as resolved.
Show resolved Hide resolved
return RecognizedWorkedPeriod(userNow, resolvedPeriod, minutes, timeZone);
}
var thisMorning = DateTime.Today.AddHours(9);
return (thisMorning, thisMorning.AddMinutes(minutes));

var thisMorning = userNow.Date.AddHours(9);
var thisMorningUtc = TimeZoneInfo.ConvertTimeToUtc(thisMorning, timeZone);
return (thisMorningUtc, thisMorningUtc.AddMinutes(minutes));
}

private static (DateTime start, DateTime end) RecognizedWorkedPeriod(DateTime refTime,
IReadOnlyDictionary<string, string> periodData, double minutes, TimeZoneInfo timeZone)
{
string dateTimeType = periodData["type"];
if (dateTimeType.Equals("date"))
{
var date = DateTime.Parse(periodData["value"]);
var start = new DateTime(date.Year, date.Month, date.Day, 9, 0, 0);
var startUtc = TimeZoneInfo.ConvertTimeToUtc(start, timeZone);
return (startUtc, startUtc.AddMinutes(minutes));
}

if (dateTimeType.Equals("datetime"))
{
var datetime = ParseToUtc(periodData["value"], timeZone);
return (datetime, datetime.AddMinutes(minutes));
}

if (dateTimeType.Equals("timerange") && periodData.ContainsKey("Mod") && periodData["Mod"].Equals("before"))
{
var time = ParseToUtc(periodData["end"], timeZone);
var datetime = new DateTime(refTime.Year, refTime.Month, refTime.Day, time.Hour, time.Minute,
time.Second);
return (datetime.Subtract(TimeSpan.FromMinutes(minutes)), datetime);
}

if (dateTimeType.Equals("timerange") && periodData.ContainsKey("Mod") && periodData["Mod"].Equals("since"))
{
var time = ParseToUtc(periodData["start"], timeZone);
var datetime = new DateTime(refTime.Year, refTime.Month, refTime.Day, time.Hour, time.Minute,
time.Second);
return (datetime, datetime.AddMinutes(minutes));
}

if (dateTimeType.Equals("timerange") && !periodData.ContainsKey("Mod"))
{
var timeStart = ParseToUtc(periodData["start"], timeZone);
var datetimeStart = new DateTime(refTime.Year, refTime.Month, refTime.Day, timeStart.Hour,
timeStart.Minute, timeStart.Second);
var timeEnd = ParseToUtc(periodData["end"], timeZone);
var datetimeEnd = new DateTime(refTime.Year, refTime.Month, refTime.Day, timeEnd.Hour,
timeEnd.Minute, timeEnd.Second);

double minutesBetweenDates = datetimeEnd.Subtract(datetimeStart).TotalMinutes;
// Floating point comparison, we check that the difference is greater than one minute.
if (Math.Abs(minutesBetweenDates - minutes) > 1)
{
throw new InvalidWorkedPeriodException(
$"Worked period time span differs from the duration provided. Expected {minutes} but got {minutesBetweenDates}");
}

return (datetimeStart, datetimeEnd);
}

throw new InvalidWorkedPeriodException($"Date time type {dateTimeType} is not allowed");
}

private static DateTime ParseToUtc(string localDateTimeString, TimeZoneInfo timeZone)
{
var localDt = DateTime.Parse(localDateTimeString);
return TimeZoneInfo.ConvertTimeToUtc(localDt, timeZone);
}
}
}
Loading