Skip to content

Commit

Permalink
feat(ux): ✨ implement reports command (#2) (#13)
Browse files Browse the repository at this point in the history
Squashed commit of the following:

commit 0da4d1f
Author: James <james@jamesnzl.xyz>
Date:   Thu May 18 22:09:14 2023 +1200

    fix(reports): :bug: fix missed name kebaberisation

commit b6e4a0d
Author: James <james@jamesnzl.xyz>
Date:   Thu May 18 22:06:41 2023 +1200

    feat(reports): :sparkles: implement viewing of past reports (#2)

commit 934acee
Author: James <james@jamesnzl.xyz>
Date:   Thu May 18 19:53:47 2023 +1200

    fix(reports): :speech_balloon: fix incorrect time strings when 0 (#11)

commit ff8b231
Merge: 34df66f 45a10c3
Author: James <james@jamesnzl.xyz>
Date:   Thu May 18 19:52:51 2023 +1200

    Merge branch 'main' into tgl-view

commit 34df66f
Author: James <james@jamesnzl.xyz>
Date:   Thu May 18 19:48:13 2023 +1200

    refactor(reports): :rotating_light: remove `pragma` directive

commit 5a3bb60
Merge: 94f7723 bd14f00
Author: James <james@jamesnzl.xyz>
Date:   Thu May 18 19:41:35 2023 +1200

    Merge branch 'main' into tgl-view

commit 94f7723
Author: James <james@jamesnzl.xyz>
Date:   Thu May 18 18:47:50 2023 +1200

    style: :art: add parentheses around boolean expressions

commit e28ffca
Author: James <james@jamesnzl.xyz>
Date:   Thu May 18 16:40:14 2023 +1200

    refactor(reports): :truck: rename `view` -> `reports` (#2)

commit a8bd335
Author: James <james@jamesnzl.xyz>
Date:   Thu May 18 16:31:37 2023 +1200

    fix(view): :bug: fix autocomplete of `projects`/`clients` sub selection

commit 0d09619
Merge: 2d0a793 7d2cc3f
Author: James <james@jamesnzl.xyz>
Date:   Thu May 18 16:21:50 2023 +1200

    Merge branch 'main' into tgl-view

commit 7d2cc3f
Author: James <james@jamesnzl.xyz>
Date:   Thu May 18 16:19:36 2023 +1200

    perf: :zap: use semaphores to disallow new fetch while another is in progress (#10)

commit 2d0a793
Author: James <james@jamesnzl.xyz>
Date:   Thu May 18 13:26:13 2023 +1200

    feat(view): :card_file_box: invalidate reports caches on command actions

commit 50148e5
Author: James <james@jamesnzl.xyz>
Date:   Thu May 18 11:42:23 2023 +1200

    fix(view): :bug: ensure total results have highest score if only one result

commit be8b068
Author: James <james@jamesnzl.xyz>
Date:   Thu May 18 11:40:17 2023 +1200

    feat(view): :lipstick: add subtitle to subtotal results with project/client name

commit 8bd7bd9
Author: James <james@jamesnzl.xyz>
Date:   Thu May 18 11:32:48 2023 +1200

    feat(view): :children_crossing: display selected clients's projects

commit 6f4293e
Author: James <james@jamesnzl.xyz>
Date:   Thu May 18 10:47:04 2023 +1200

    feat(view): :children_crossing: set `AutoCompleteText` for totals result

commit 7a90598
Author: James <james@jamesnzl.xyz>
Date:   Thu May 18 10:42:18 2023 +1200

    feat(view): :children_crossing: time entries `Action` passes to `start` command

commit 3cfe32a
Author: James <james@jamesnzl.xyz>
Date:   Thu May 18 10:38:04 2023 +1200

    fix(view): :bug: display total time tracked for selected project

commit 5eebf82
Author: James <james@jamesnzl.xyz>
Date:   Thu May 18 01:05:21 2023 +1200

    feat(view): :children_crossing: display selected project's time entries

commit 86d47c4
Author: James <james@jamesnzl.xyz>
Date:   Thu May 18 00:44:56 2023 +1200

    feat(view): :children_crossing: implement searching for result title and autocomplete

commit a766df3
Author: James <james@jamesnzl.xyz>
Date:   Thu May 18 00:40:53 2023 +1200

    refactor: :mute: remove `cacheKey` log

commit 04d0b70
Author: James <james@jamesnzl.xyz>
Date:   Thu May 18 00:23:04 2023 +1200

    perf(view): :zap: do not re-create background fetches when searching for span

commit 8dee4b4
Merge: 5653f6d 864092b
Author: James <james@jamesnzl.xyz>
Date:   Thu May 18 00:20:10 2023 +1200

    Merge branch 'main' into tgl-view

commit 5653f6d
Author: James <james@jamesnzl.xyz>
Date:   Thu May 18 00:10:32 2023 +1200

    fix(view): :bug: fix mutation of cached report data with currently running timer

commit 32a5e68
Author: James <james@jamesnzl.xyz>
Date:   Wed May 17 23:46:19 2023 +1200

    feat(view): :construction: cache summary time entries requests

commit 07bf460
Merge: b61a4ab 086bf25
Author: James <james@jamesnzl.xyz>
Date:   Wed May 17 23:29:17 2023 +1200

    Merge branch 'main' into tgl-view

commit b61a4ab
Author: James <james@jamesnzl.xyz>
Date:   Wed May 17 22:53:38 2023 +1200

    refactor(view): :pencil2: rename `timeEntries` to `summary`

commit bf42eff
Author: James <james@jamesnzl.xyz>
Date:   Wed May 17 22:51:53 2023 +1200

    feat(view): :sparkles: include running timer in `tgl view` reports

commit 114e8bd
Merge: 60af653 3b0b391
Author: James <james@jamesnzl.xyz>
Date:   Wed May 17 14:34:54 2023 +1200

    Merge branch 'main' into tgl-view

commit 60af653
Author: James <james@jamesnzl.xyz>
Date:   Wed May 17 09:15:52 2023 +1200

    feat(view): :lipstick: use most tracked project colour for client icon

commit 6a701fa
Author: James <james@jamesnzl.xyz>
Date:   Wed May 17 09:09:12 2023 +1200

    feat(view): :sparkles: implement `tgl view <span> entries`

commit b4ed9ce
Merge: eaea4a0 44d2a29
Author: James <james@jamesnzl.xyz>
Date:   Wed May 17 08:54:44 2023 +1200

    Merge branch 'main' into tgl-view

commit eaea4a0
Author: James <james@jamesnzl.xyz>
Date:   Tue May 16 23:34:29 2023 +1200

    fix(view): :bug: fix year span start date calculation

commit 59a0c5c
Author: James <james@jamesnzl.xyz>
Date:   Tue May 16 23:30:24 2023 +1200

    feat(view): :sparkles: extend implementation to `tgl view <span> clients`

commit db659d2
Author: James <james@jamesnzl.xyz>
Date:   Tue May 16 23:08:34 2023 +1200

    feat(view): :construction: use `POST` search time entries endpoint

    this single endpoint should support the needs of all the different `view` sub-commands

commit 48894da
Merge: 98f84f9 46c909d
Author: James <james@jamesnzl.xyz>
Date:   Tue May 16 22:04:08 2023 +1200

    Merge branch 'main' into tgl-view

commit 98f84f9
Author: James <james@jamesnzl.xyz>
Date:   Tue May 16 21:55:34 2023 +1200

    fix(view): :bug: fix incorrect time strings when longer than 1 day

    define Humanizer's `maxUnit` to `Hours`

commit a99d16e
Merge: 861e77d 2cc6564
Author: James <james@jamesnzl.xyz>
Date:   Tue May 16 21:54:54 2023 +1200

    Merge branch 'main' into tgl-view

commit 861e77d
Author: James <james@jamesnzl.xyz>
Date:   Tue May 16 16:20:03 2023 +1200

    perf: :zap: avoid reconstructing query string with `string.Join()` when possible

commit 67f9d56
Author: James <james@jamesnzl.xyz>
Date:   Tue May 16 14:23:23 2023 +1200

    refactor: :art: remove hardcoded literal indices for command arguments

commit 6bb5d8f
Author: James <james@jamesnzl.xyz>
Date:   Tue May 16 12:18:03 2023 +1200

    feat(view): :children_crossing: rank groupings as projects -> clients -> entries

commit 3c5b2f0
Author: James <james@jamesnzl.xyz>
Date:   Tue May 16 10:44:03 2023 +1200

    feat(view): :sparkles: finish rough implementation of `tgl view day projects`

commit 217f8f1
Author: James <james@jamesnzl.xyz>
Date:   Tue May 16 09:46:02 2023 +1200

    fix(view): :bug: set `CommandArgument` members to be `init` only

commit cdfdf43
Author: James <james@jamesnzl.xyz>
Date:   Tue May 16 09:34:03 2023 +1200

    refactor(view): :recycle: rename durations -> spans

commit 4c78d72
Author: James <james@jamesnzl.xyz>
Date:   Mon May 15 23:58:32 2023 +1200

    feat(toggl): :bug: use reports api base url

commit 8e4d33c
Author: James <james@jamesnzl.xyz>
Date:   Mon May 15 23:52:58 2023 +1200

    fix(toggl): :bug: `start_date` may not be null

commit f801621
Author: James <james@jamesnzl.xyz>
Date:   Mon May 15 23:47:29 2023 +1200

    feat(view): :construction: use `Dictionary` for collection of `CommandArgument`s

commit 091f5be
Author: James <james@jamesnzl.xyz>
Date:   Mon May 15 23:25:46 2023 +1200

    feat(toggl): :sparkles: implement list project users reports api method

commit d8e2ca2
Author: James <james@jamesnzl.xyz>
Date:   Mon May 15 20:29:03 2023 +1200

    feat(view): :construction: implement report grouping selection

commit 742e59d
Merge: 5d663a7 fbba79e
Author: James <james@jamesnzl.xyz>
Date:   Mon May 15 20:15:49 2023 +1200

    Merge branch 'main' into tgl-view

commit 5d663a7
Author: James <james@jamesnzl.xyz>
Date:   Mon May 15 20:15:07 2023 +1200

    refactor(view): :recycle: refactor `CommandArgument` configuration class

commit e2f4286
Author: James <james@jamesnzl.xyz>
Date:   Mon May 15 11:24:49 2023 +1200

    feat(view): :construction: implement foundations of `tgl view` command (#2)

commit f7c301c
Author: James <james@jamesnzl.xyz>
Date:   Mon May 15 11:24:20 2023 +1200

    chore(assets): :bento: add view icon
  • Loading branch information
JamesNZL committed May 18, 2023
1 parent 45a10c3 commit 2203495
Show file tree
Hide file tree
Showing 7 changed files with 1,028 additions and 89 deletions.
Binary file added assets/reports.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
29 changes: 29 additions & 0 deletions assets/svg/reports.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
12 changes: 11 additions & 1 deletion src/Main.cs
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
using System.Windows.Controls;
using System.Collections.Generic;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using Flow.Launcher.Plugin.TogglTrack.ViewModels;
Expand All @@ -18,6 +19,14 @@ public class Main : IAsyncPlugin, ISettingProvider

internal TogglTrack? _togglTrack;

public static string ExtractFromQuery(Query query, int index)
{
return (index == 1)
// Expect slight performance improvement by using query.SecondToEndSearch directly
? query.SecondToEndSearch
: string.Join(" ", query.SearchTerms.Skip(index));
}

/// <summary>
/// Runs on plugin initialisation.
/// Expensive operations should be performed here.
Expand Down Expand Up @@ -72,13 +81,14 @@ public async Task<List<Result>> QueryAsync(Query query, CancellationToken token)
return await this._togglTrack.GetDefaultHotKeys();
}

return query.FirstSearch.ToLower() switch
return (query.FirstSearch.ToLower()) switch
{
Settings.StartCommand => await this._togglTrack.RequestStartEntry(token, query),
Settings.EditCommand => await this._togglTrack.RequestEditEntry(token, query),
Settings.StopCommand => await this._togglTrack.RequestStopEntry(token, query),
Settings.DeleteCommand => await this._togglTrack.RequestDeleteEntry(token),
Settings.ContinueCommand => await this._togglTrack.RequestContinueEntry(token, query),
Settings.ReportsCommand => await this._togglTrack.RequestViewReports(token, query),
_ => (await this._togglTrack.GetDefaultHotKeys())
.FindAll(result =>
{
Expand Down
187 changes: 187 additions & 0 deletions src/Settings.cs
Original file line number Diff line number Diff line change
@@ -1,3 +1,7 @@
using System;
using System.Text.RegularExpressions;
using System.Collections.Generic;

namespace Flow.Launcher.Plugin.TogglTrack
{
/// <Summary>
Expand All @@ -10,15 +14,198 @@ public class Settings
internal const string StopCommand = "stop";
internal const string DeleteCommand = "delete";
internal const string ContinueCommand = "continue";
internal const string ReportsCommand = "reports";
internal const string BrowserCommand = "browser";
internal const string RefreshCommand = "refresh";

internal const string EditProjectFlag = "-p";
internal const string TimeSpanFlag = "-t";

internal enum ReportsSpanKeys
{
Day,
Week,
Month,
Year,
}
internal static readonly Regex ReportsSpanOffsetRegex = new Regex(@"-(\d+)");
internal static readonly List<ReportsSpanCommandArgument> ReportsSpanArguments = new List<ReportsSpanCommandArgument>
{
new ReportsSpanCommandArgument
{
Argument = "day",
Interpolation = offset =>
{
switch (offset)
{
case (0):
{
return "today";
}
case (1):
{
return "yesterday";
}
default:
{
return $"{offset} days ago";
}
}
},
Score = 400,
// Offsetted day
Start = (referenceDate, offset) => referenceDate.AddDays(-offset),
End = (referenceDate, offset) => referenceDate.AddDays(-offset),
},
new ReportsSpanCommandArgument
{
Argument = "week",
Interpolation = offset =>
{
switch (offset)
{
case (0):
{
return "this week";
}
case (1):
{
return "last week";
}
default:
{
return $"{offset} weeks ago";
}
}
},
Score = 300,
// Monday of the offsetted week
Start = (referenceDate, offset) => referenceDate.AddDays((-(int)referenceDate.DayOfWeek + 1) - (7 * offset)),
// Sunday of the offsetted week
End = (referenceDate, offset) => referenceDate.AddDays((-(int)referenceDate.DayOfWeek + 7) - (7 * offset)),
},
new ReportsSpanCommandArgument
{
Argument = "month",
Interpolation = offset =>
{
switch (offset)
{
case (0):
{
return "this month";
}
case (1):
{
return "last month";
}
default:
{
return $"{offset} months ago";
}
}
},
Score = 200,
// First day of the offsetted month
Start = (referenceDate, offset) => new DateTimeOffset(referenceDate.Year, referenceDate.Month, 1, 0, 0, 0, referenceDate.Offset).AddMonths(-offset),
// Last day of the offsetted month
End = (referenceDate, offset) => new DateTimeOffset(referenceDate.Year, referenceDate.Month, DateTime.DaysInMonth(referenceDate.Year, referenceDate.Month), 0, 0, 0, referenceDate.Offset).AddMonths(-offset),
},
new ReportsSpanCommandArgument
{
Argument = "year",
Interpolation = offset =>
{
switch (offset)
{
case (0):
{
return "this year";
}
case (1):
{
return "last year";
}
default:
{
return $"{offset} years ago";
}
}
},
Score = 100,
// First day of the offsetted year
Start = (referenceDate, offset) => new DateTimeOffset(referenceDate.Year - offset, 1, 1, 0, 0, 0, referenceDate.Offset),
// Last day of the offsetted year
End = (referenceDate, offset) => new DateTimeOffset(referenceDate.Year - offset, 1, 1, 0, 0, 0, referenceDate.Offset),
},
};

public enum ReportsGroupingKeys
{
Projects,
Clients,
Entries,
}
private const string ReportsGroupingProjectsArgument = "projects";
private const string ReportsGroupingClientsArgument = "clients";
private const string ReportsGroupingEntriesArgument = "entries";
internal static readonly List<ReportsGroupingCommandArgument> ReportsGroupingArguments = new List<ReportsGroupingCommandArgument>
{
new ReportsGroupingCommandArgument
{
Argument = Settings.ReportsGroupingProjectsArgument,
Interpolation = "View tracked time grouped by project",
Score = 300,
Grouping = Settings.ReportsGroupingKeys.Projects,
SubArgument = null,
},
new ReportsGroupingCommandArgument
{
Argument = Settings.ReportsGroupingClientsArgument,
Interpolation = "View tracked time grouped by client",
Score = 200,
Grouping = Settings.ReportsGroupingKeys.Clients,
SubArgument = Settings.ReportsGroupingProjectsArgument,
},
new ReportsGroupingCommandArgument
{
Argument = Settings.ReportsGroupingEntriesArgument,
Interpolation = "View tracked time entries",
Score = 100,
Grouping = Settings.ReportsGroupingKeys.Entries,
SubArgument = null,
},
};

/// <Summary>
/// Toggl Track API Token.
/// </Summary>
public string ApiToken { get; set; } = string.Empty;
}

public class CommandArgument
{
#nullable disable
public string Argument { get; init; }
public string Interpolation { get; init; }
public int Score { get; init; }
#nullable enable
}

public class ReportsSpanCommandArgument : CommandArgument
{
#nullable disable
public new Func<int, string> Interpolation { get; init; }
public Func<DateTimeOffset, int, DateTimeOffset> Start { get; init; }
public Func<DateTimeOffset, int, DateTimeOffset> End { get; init; }
#nullable enable
}

public class ReportsGroupingCommandArgument : CommandArgument
{
#nullable disable
public Settings.ReportsGroupingKeys Grouping { get; init; }
public string SubArgument { get; init; }
#nullable enable
}
}
74 changes: 53 additions & 21 deletions src/Toggl/Client.cs
Original file line number Diff line number Diff line change
Expand Up @@ -8,18 +8,26 @@ namespace Flow.Launcher.Plugin.TogglTrack.TogglApi
public class TogglClient
{
private readonly static string _baseUrl = "https://api.track.toggl.com/api/v9/";
private readonly static string _reportsUrl = "https://api.track.toggl.com/reports/api/v3/";
private readonly AuthenticatedFetch _api;
private readonly AuthenticatedFetch _reportsApi;

public TogglClient(string token)
{
this._api = new AuthenticatedFetch(token, TogglClient._baseUrl);
this._reportsApi = new AuthenticatedFetch(token, TogglClient._reportsUrl);
}

public void UpdateToken(string token)
{
this._api.UpdateToken(token);
this._reportsApi.UpdateToken(token);
}

/*
* Standard API
*/

public async Task<Me?> GetMe()
{
return await this._api.Get<Me>("me?with_related_data=true");
Expand All @@ -42,16 +50,16 @@ public void UpdateToken(string token)
: DateTimeOffset.UtcNow;

return await this._api.Post<TimeEntry>($"workspaces/{workspaceId}/time_entries", new
{
billable,
created_with = "flow-toggl-plugin",
description,
duration = -1 * dateTimeOffset.ToUnixTimeSeconds(),
project_id = projectId ?? default(long?),
start = dateTimeOffset.ToString("yyyy-MM-ddTHH:mm:ssZ"),
tags,
workspace_id = workspaceId,
});
{
billable,
created_with = "flow-toggl-plugin",
description,
duration = -1 * dateTimeOffset.ToUnixTimeSeconds(),
project_id = projectId ?? default(long?),
start = dateTimeOffset.ToString("yyyy-MM-ddTHH:mm:ssZ"),
tags,
workspace_id = workspaceId,
});
}

public async Task<TimeEntry?> EditTimeEntry(TimeEntry timeEntry, long? projectId, string? description, DateTimeOffset? start, DateTimeOffset? stop)
Expand All @@ -67,17 +75,17 @@ public void UpdateToken(string token)
}

return await this._api.Put<TimeEntry>($"workspaces/{timeEntry.workspace_id}/time_entries/{timeEntry.id}", new
{
timeEntry.billable,
created_with = "flow-toggl-plugin",
description,
duration,
project_id = projectId,
start = start?.ToString("yyyy-MM-ddTHH:mm:ssZ"),
stop = stop?.ToString("yyyy-MM-ddTHH:mm:ssZ"),
timeEntry.tags,
timeEntry.workspace_id,
});
{
timeEntry.billable,
created_with = "flow-toggl-plugin",
description,
duration,
project_id = projectId,
start = start?.ToString("yyyy-MM-ddTHH:mm:ssZ"),
stop = stop?.ToString("yyyy-MM-ddTHH:mm:ssZ"),
timeEntry.tags,
timeEntry.workspace_id,
});
}

public async Task<TimeEntry?> GetRunningTimeEntry()
Expand Down Expand Up @@ -109,5 +117,29 @@ public void UpdateToken(string token)
{
return await this._api.Get<List<TimeEntry>>($"me/time_entries");
}

/*
* Reports API
*/

public async Task<SummaryTimeEntry?> GetSummaryTimeEntries(long workspaceId, long userId, Settings.ReportsGroupingKeys reportGrouping, DateTimeOffset start, DateTimeOffset? end)
{
(string grouping, string sub_grouping) = (reportGrouping) switch
{
Settings.ReportsGroupingKeys.Projects => ("projects", "time_entries"),
Settings.ReportsGroupingKeys.Clients => ("clients", "projects"),
Settings.ReportsGroupingKeys.Entries => ("projects", "time_entries"),
_ => ("projects", "time_entries"),
};

return await this._reportsApi.Post<SummaryTimeEntry>($"workspace/{workspaceId}/summary/time_entries", new
{
user_ids = new long[] { userId },
start_date = start.ToString("yyyy-MM-dd"),
end_date = end?.ToString("yyyy-MM-dd"),
grouping,
sub_grouping,
});
}
}
}
Loading

0 comments on commit 2203495

Please sign in to comment.