From 13d9d2dfe2e40fda2d97f854e2ad40ff139a027a Mon Sep 17 00:00:00 2001 From: Boudi Date: Wed, 30 Sep 2026 12:52:27 +0200 Subject: [PATCH 01/17] Create basic structure for the project --- CodeReviews.Console.CodingTracker.sln | 22 +++++++++++++++++++ CodingTracker/CodingTracker.csproj | 10 +++++++++ CodingTracker/Program.cs | 3 +++ CodingTrackerTests/CodingTrackerTests.csproj | 23 ++++++++++++++++++++ CodingTrackerTests/UnitTest1.cs | 15 +++++++++++++ README.md | 1 + 6 files changed, 74 insertions(+) create mode 100644 CodeReviews.Console.CodingTracker.sln create mode 100644 CodingTracker/CodingTracker.csproj create mode 100644 CodingTracker/Program.cs create mode 100644 CodingTrackerTests/CodingTrackerTests.csproj create mode 100644 CodingTrackerTests/UnitTest1.cs create mode 100644 README.md diff --git a/CodeReviews.Console.CodingTracker.sln b/CodeReviews.Console.CodingTracker.sln new file mode 100644 index 000000000..7d32bc458 --- /dev/null +++ b/CodeReviews.Console.CodingTracker.sln @@ -0,0 +1,22 @@ + +Microsoft Visual Studio Solution File, Format Version 12.00 +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "CodingTracker", "CodingTracker\CodingTracker.csproj", "{514F39B8-9759-4A04-918A-C2B2C4E12D0B}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "CodingTrackerTests", "CodingTrackerTests\CodingTrackerTests.csproj", "{5104A8EC-03BB-4927-9FFF-7B33E0132E44}" +EndProject +Global + GlobalSection(SolutionConfigurationPlatforms) = preSolution + Debug|Any CPU = Debug|Any CPU + Release|Any CPU = Release|Any CPU + EndGlobalSection + GlobalSection(ProjectConfigurationPlatforms) = postSolution + {514F39B8-9759-4A04-918A-C2B2C4E12D0B}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {514F39B8-9759-4A04-918A-C2B2C4E12D0B}.Debug|Any CPU.Build.0 = Debug|Any CPU + {514F39B8-9759-4A04-918A-C2B2C4E12D0B}.Release|Any CPU.ActiveCfg = Release|Any CPU + {514F39B8-9759-4A04-918A-C2B2C4E12D0B}.Release|Any CPU.Build.0 = Release|Any CPU + {5104A8EC-03BB-4927-9FFF-7B33E0132E44}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {5104A8EC-03BB-4927-9FFF-7B33E0132E44}.Debug|Any CPU.Build.0 = Debug|Any CPU + {5104A8EC-03BB-4927-9FFF-7B33E0132E44}.Release|Any CPU.ActiveCfg = Release|Any CPU + {5104A8EC-03BB-4927-9FFF-7B33E0132E44}.Release|Any CPU.Build.0 = Release|Any CPU + EndGlobalSection +EndGlobal diff --git a/CodingTracker/CodingTracker.csproj b/CodingTracker/CodingTracker.csproj new file mode 100644 index 000000000..6c1dc922b --- /dev/null +++ b/CodingTracker/CodingTracker.csproj @@ -0,0 +1,10 @@ + + + + Exe + net10.0 + enable + enable + + + diff --git a/CodingTracker/Program.cs b/CodingTracker/Program.cs new file mode 100644 index 000000000..e5dff12bc --- /dev/null +++ b/CodingTracker/Program.cs @@ -0,0 +1,3 @@ +// See https://aka.ms/new-console-template for more information + +Console.WriteLine("Hello, World!"); \ No newline at end of file diff --git a/CodingTrackerTests/CodingTrackerTests.csproj b/CodingTrackerTests/CodingTrackerTests.csproj new file mode 100644 index 000000000..68b9f9b9b --- /dev/null +++ b/CodingTrackerTests/CodingTrackerTests.csproj @@ -0,0 +1,23 @@ + + + + net10.0 + latest + enable + enable + false + + + + + + + + + + + + + + + diff --git a/CodingTrackerTests/UnitTest1.cs b/CodingTrackerTests/UnitTest1.cs new file mode 100644 index 000000000..735a76c88 --- /dev/null +++ b/CodingTrackerTests/UnitTest1.cs @@ -0,0 +1,15 @@ +namespace CodingTrackerTests; + +public class Tests +{ + [SetUp] + public void Setup() + { + } + + [Test] + public void Test1() + { + Assert.Pass(); + } +} \ No newline at end of file diff --git a/README.md b/README.md new file mode 100644 index 000000000..44af65076 --- /dev/null +++ b/README.md @@ -0,0 +1 @@ +# Coding Tracker From 64d1e594099060d1907f6960bb40ad5b82b7f414 Mon Sep 17 00:00:00 2001 From: Boudi Date: Wed, 30 Sep 2026 13:08:09 +0200 Subject: [PATCH 02/17] Add appsettings.json and a way to load it --- CodingTracker/AppSettings.cs | 26 +++++++++++++++++++ CodingTracker/CodingTracker.csproj | 5 ++++ CodingTracker/Program.cs | 10 +++++-- .../{UnitTest1.cs => AppSettingsTests.cs} | 0 appsettings.json | 3 +++ 5 files changed, 42 insertions(+), 2 deletions(-) create mode 100644 CodingTracker/AppSettings.cs rename CodingTrackerTests/{UnitTest1.cs => AppSettingsTests.cs} (100%) create mode 100644 appsettings.json diff --git a/CodingTracker/AppSettings.cs b/CodingTracker/AppSettings.cs new file mode 100644 index 000000000..9176919bb --- /dev/null +++ b/CodingTracker/AppSettings.cs @@ -0,0 +1,26 @@ +namespace CodingTracker; + +using System; +using System.IO; +using System.Text.Json; + +public class AppSettings +{ + public string DbPath { get; set; } = string.Empty; + + private static readonly string FilePath = Path.Combine(AppContext.BaseDirectory, "config.json"); + + public static AppSettings LoadConfig() + { + if (!File.Exists(FilePath)) + { + throw new FileNotFoundException($"Configuration file missing at: {FilePath}"); + } + + string jsonContent = File.ReadAllText(FilePath); + + return JsonSerializer.Deserialize(jsonContent) + ?? throw new InvalidOperationException("Failed to deserialize configuration file."); + } + +} diff --git a/CodingTracker/CodingTracker.csproj b/CodingTracker/CodingTracker.csproj index 6c1dc922b..91c97662a 100644 --- a/CodingTracker/CodingTracker.csproj +++ b/CodingTracker/CodingTracker.csproj @@ -7,4 +7,9 @@ enable + + + PreserveNewest + + diff --git a/CodingTracker/Program.cs b/CodingTracker/Program.cs index e5dff12bc..99c225778 100644 --- a/CodingTracker/Program.cs +++ b/CodingTracker/Program.cs @@ -1,3 +1,9 @@ -// See https://aka.ms/new-console-template for more information +namespace CodingTracker; -Console.WriteLine("Hello, World!"); \ No newline at end of file +static class Program +{ + public static void Main(string[] args) + { + Console.WriteLine("Hello, World!"); + } +} \ No newline at end of file diff --git a/CodingTrackerTests/UnitTest1.cs b/CodingTrackerTests/AppSettingsTests.cs similarity index 100% rename from CodingTrackerTests/UnitTest1.cs rename to CodingTrackerTests/AppSettingsTests.cs diff --git a/appsettings.json b/appsettings.json new file mode 100644 index 000000000..0e0dcd235 --- /dev/null +++ b/appsettings.json @@ -0,0 +1,3 @@ +{ + +} \ No newline at end of file From f9bf4a8ce37fec24e90639c3e34bd5aa7f35625e Mon Sep 17 00:00:00 2001 From: Boudi Date: Wed, 30 Sep 2026 13:24:13 +0200 Subject: [PATCH 03/17] Add CodingSession class + way to show it in a table in spectre.console --- CodingTracker/CodingSession.cs | 29 +++++++++++++++++++++++++++++ CodingTracker/CodingTracker.csproj | 6 ++++++ 2 files changed, 35 insertions(+) create mode 100644 CodingTracker/CodingSession.cs diff --git a/CodingTracker/CodingSession.cs b/CodingTracker/CodingSession.cs new file mode 100644 index 000000000..7a26e86e1 --- /dev/null +++ b/CodingTracker/CodingSession.cs @@ -0,0 +1,29 @@ +using System.Globalization; +using Spectre.Console; + +namespace CodingTracker; + +public class CodingSession +{ + private int Id { get; set; } + private DateTime StartTime { get; set; } + private DateTime EndTime { get; set; } + private TimeSpan Duration { get; set; } + + public CodingSession(int id, DateTime startTime, DateTime endTime, TimeSpan duration) + { + Id = id; + StartTime = startTime; + EndTime = endTime; + Duration = duration; + } + + public void AddRowToTable(Table table) + { + table.AddRow(Id.ToString(), + StartTime.ToString(CultureInfo.CurrentCulture), + EndTime.ToString(CultureInfo.CurrentCulture), + Duration.ToString()); + } + +} \ No newline at end of file diff --git a/CodingTracker/CodingTracker.csproj b/CodingTracker/CodingTracker.csproj index 91c97662a..e9122c919 100644 --- a/CodingTracker/CodingTracker.csproj +++ b/CodingTracker/CodingTracker.csproj @@ -12,4 +12,10 @@ PreserveNewest + + + + + + From d995d6880eeace98e1cc336e3b38c839f170006a Mon Sep 17 00:00:00 2001 From: Boudi Date: Wed, 30 Sep 2026 13:26:39 +0200 Subject: [PATCH 04/17] Value for appsettings + test --- CodingTrackerTests/AppSettingsTests.cs | 7 ++++++- CodingTrackerTests/CodingTrackerTests.csproj | 4 ++++ appsettings.json | 2 +- 3 files changed, 11 insertions(+), 2 deletions(-) diff --git a/CodingTrackerTests/AppSettingsTests.cs b/CodingTrackerTests/AppSettingsTests.cs index 735a76c88..210ea2c74 100644 --- a/CodingTrackerTests/AppSettingsTests.cs +++ b/CodingTrackerTests/AppSettingsTests.cs @@ -1,15 +1,20 @@ namespace CodingTrackerTests; +using CodingTracker; public class Tests { + private AppSettings validAppSettings; + [SetUp] public void Setup() { + validAppSettings = AppSettings.LoadConfig(); } [Test] public void Test1() { - Assert.Pass(); + string expectedDbPath = "CodingTracker.db"; + Assert.Equals(validAppSettings.DbPath, expectedDbPath); } } \ No newline at end of file diff --git a/CodingTrackerTests/CodingTrackerTests.csproj b/CodingTrackerTests/CodingTrackerTests.csproj index 68b9f9b9b..bcc4d1599 100644 --- a/CodingTrackerTests/CodingTrackerTests.csproj +++ b/CodingTrackerTests/CodingTrackerTests.csproj @@ -20,4 +20,8 @@ + + + + diff --git a/appsettings.json b/appsettings.json index 0e0dcd235..656a91893 100644 --- a/appsettings.json +++ b/appsettings.json @@ -1,3 +1,3 @@ { - + "DbPath": "CodingTracker.db" } \ No newline at end of file From 563c3c7ba17adb004dd33d697169b796a7ee48f4 Mon Sep 17 00:00:00 2001 From: Boudi Date: Thu, 8 Oct 2026 17:16:16 +0200 Subject: [PATCH 05/17] DataBaseInterface + add / view function --- CodingTracker/CodingSession.cs | 7 ++-- CodingTracker/CodingTracker.csproj | 1 + CodingTracker/DataBaseInterface.cs | 54 ++++++++++++++++++++++++++++++ identifier.sqlite | 0 4 files changed, 57 insertions(+), 5 deletions(-) create mode 100644 CodingTracker/DataBaseInterface.cs create mode 100644 identifier.sqlite diff --git a/CodingTracker/CodingSession.cs b/CodingTracker/CodingSession.cs index 7a26e86e1..db1459c23 100644 --- a/CodingTracker/CodingSession.cs +++ b/CodingTracker/CodingSession.cs @@ -5,14 +5,12 @@ namespace CodingTracker; public class CodingSession { - private int Id { get; set; } private DateTime StartTime { get; set; } private DateTime EndTime { get; set; } private TimeSpan Duration { get; set; } - public CodingSession(int id, DateTime startTime, DateTime endTime, TimeSpan duration) + public CodingSession(DateTime startTime, DateTime endTime, TimeSpan duration) { - Id = id; StartTime = startTime; EndTime = endTime; Duration = duration; @@ -20,10 +18,9 @@ public CodingSession(int id, DateTime startTime, DateTime endTime, TimeSpan dura public void AddRowToTable(Table table) { - table.AddRow(Id.ToString(), + table.AddRow( StartTime.ToString(CultureInfo.CurrentCulture), EndTime.ToString(CultureInfo.CurrentCulture), Duration.ToString()); } - } \ No newline at end of file diff --git a/CodingTracker/CodingTracker.csproj b/CodingTracker/CodingTracker.csproj index e9122c919..c966c1130 100644 --- a/CodingTracker/CodingTracker.csproj +++ b/CodingTracker/CodingTracker.csproj @@ -15,6 +15,7 @@ + diff --git a/CodingTracker/DataBaseInterface.cs b/CodingTracker/DataBaseInterface.cs new file mode 100644 index 000000000..2f3d50213 --- /dev/null +++ b/CodingTracker/DataBaseInterface.cs @@ -0,0 +1,54 @@ +using Dapper; +using Microsoft.Data.Sqlite; + +namespace CodingTracker; + +public static class DataBaseInterface +{ + private static readonly SqliteConnection DbConnection; + + static DataBaseInterface() + { + // TODO: Add string from appsettings.json + DbConnection = new SqliteConnection(); + DbConnection.Open(); + CreateTable(); + } + + private static void CreateTable() + { + string createTableQuery = @" + CREATE TABLE IF NOT EXISTS CodingSessions ( + Id INT PRIMARY KEY AUTOINCREMENT, + StartTime TEXT NOT NULL, + EndTime TEXT NOT NULL, + Duration INT NOT NULL);"; + + using var command = new SqliteCommand(createTableQuery, DbConnection); + command.ExecuteNonQuery(); + } + + public static async Task AddCodingSession(CodingSession codingSession) + { + string addCodingSessionQuery = @" + INSERT INTO CodingSessions (StartTime, EndTime, Duration) + VALUES (@StartTime, @EndTime, @Duration);"; + + await DbConnection.ExecuteAsync(addCodingSessionQuery, codingSession); + } + + public static async Task> ViewCodingSessions(DateTime? minStartDate = null, DateTime? maxStartDate = null) + { + var conditions = new List(); + if (minStartDate.HasValue) conditions.Add("StartTime >= @MinDate"); + if (maxStartDate.HasValue) conditions.Add("StartTime <= @MaxDate"); + + string whereClauses = conditions.Count > 0 ? " WHERE " + string.Join(" AND ", conditions) : ""; + + string viewCodingSessionsQuery = $"SELECT * FROM CodingSessions{whereClauses} ORDER BY StartTime;"; + + return (await DbConnection.QueryAsync(viewCodingSessionsQuery, + new { MinDate = minStartDate, MaxDate = maxStartDate })).ToList(); + } + +} \ No newline at end of file diff --git a/identifier.sqlite b/identifier.sqlite new file mode 100644 index 000000000..e69de29bb From fba2e80e6325326e02e294181a263b4640f1b96a Mon Sep 17 00:00:00 2001 From: Boudi Date: Thu, 8 Oct 2026 17:28:16 +0200 Subject: [PATCH 06/17] Read & use appsettings.json --- CodingTracker/ConfigSettings.cs | 30 ++++++++++++++++++++++++++++++ CodingTracker/DataBaseInterface.cs | 4 ++-- 2 files changed, 32 insertions(+), 2 deletions(-) create mode 100644 CodingTracker/ConfigSettings.cs diff --git a/CodingTracker/ConfigSettings.cs b/CodingTracker/ConfigSettings.cs new file mode 100644 index 000000000..02daede7f --- /dev/null +++ b/CodingTracker/ConfigSettings.cs @@ -0,0 +1,30 @@ +namespace CodingTracker; + +using System; +using System.IO; +using System.Text.Json; + +public class ConfigSettings +{ + public string DbPath { get; set; } = string.Empty; +} + +public static class ReadConfig +{ + public static ConfigSettings Config { get; set; } + + private static readonly string FilePath = Path.Combine(AppContext.BaseDirectory, "appsettings.json"); + + static ReadConfig() + { + if (!File.Exists(FilePath)) + { + throw new FileNotFoundException($"Configuration file missing at: {FilePath}"); + } + + string jsonContent = File.ReadAllText(FilePath); + + Config = JsonSerializer.Deserialize(jsonContent) + ?? throw new InvalidOperationException("Failed to deserialize configuration file."); + } +} \ No newline at end of file diff --git a/CodingTracker/DataBaseInterface.cs b/CodingTracker/DataBaseInterface.cs index 2f3d50213..a49668da6 100644 --- a/CodingTracker/DataBaseInterface.cs +++ b/CodingTracker/DataBaseInterface.cs @@ -9,8 +9,8 @@ public static class DataBaseInterface static DataBaseInterface() { - // TODO: Add string from appsettings.json - DbConnection = new SqliteConnection(); + string dbPath = ReadConfig.Config.DbPath; + DbConnection = new SqliteConnection(dbPath); DbConnection.Open(); CreateTable(); } From 8a5e8b6719eefc5b1fa6a42470e804af909b2586 Mon Sep 17 00:00:00 2001 From: Boudi Date: Thu, 8 Oct 2026 18:03:11 +0200 Subject: [PATCH 07/17] Basic controller --- CodingTracker/Controller.cs | 18 ++++++++++++++++++ CodingTracker/Program.cs | 4 ++-- 2 files changed, 20 insertions(+), 2 deletions(-) create mode 100644 CodingTracker/Controller.cs diff --git a/CodingTracker/Controller.cs b/CodingTracker/Controller.cs new file mode 100644 index 000000000..50eacb697 --- /dev/null +++ b/CodingTracker/Controller.cs @@ -0,0 +1,18 @@ +namespace CodingTracker; + +public static class Controller +{ + public static async Task Start() + { + Console.WriteLine("Hello, World!"); + } + + + public static int Duration(DateTime start, DateTime end) + { + if (start.CompareTo(end) >= 0) throw new Exception("Start must be earlier than end"); + + return (end - start).Seconds; + } + +} \ No newline at end of file diff --git a/CodingTracker/Program.cs b/CodingTracker/Program.cs index 99c225778..0548acb65 100644 --- a/CodingTracker/Program.cs +++ b/CodingTracker/Program.cs @@ -2,8 +2,8 @@ static class Program { - public static void Main(string[] args) + public static async Task Main(string[] args) { - Console.WriteLine("Hello, World!"); + await Controller.Start(); } } \ No newline at end of file From cce38b15cca1d2412effdf02f110e32f9122265e Mon Sep 17 00:00:00 2001 From: Boudi Date: Fri, 9 Oct 2026 02:11:26 +0200 Subject: [PATCH 08/17] Add UserIntraface taht creates Menu /prompt to interact with User --- CodingTracker/UserInterface.cs | 49 +++++++++++++++++++++++++++ CodingTracker/UserOption.cs | 42 +++++++++++++++++++++++ CodingTracker/Validation.cs | 13 +++++++ CodingTrackerTests/ValidationTests.cs | 27 +++++++++++++++ 4 files changed, 131 insertions(+) create mode 100644 CodingTracker/UserInterface.cs create mode 100644 CodingTracker/UserOption.cs create mode 100644 CodingTracker/Validation.cs create mode 100644 CodingTrackerTests/ValidationTests.cs diff --git a/CodingTracker/UserInterface.cs b/CodingTracker/UserInterface.cs new file mode 100644 index 000000000..d72d04a70 --- /dev/null +++ b/CodingTracker/UserInterface.cs @@ -0,0 +1,49 @@ +using Spectre.Console; + +namespace CodingTracker; + +public static class UserInterface +{ + private const string TitleStyle = "[green]"; + private const string MainMenuTitle = $"{TitleStyle}What do you want to do ?[/]"; + private const string ViewMenuTitle = $"{TitleStyle}Navigate through the months[/]"; + + private static readonly string ConfirmationBaseMessage = $"Are you sure you want to {{0}} ?"; + + private static readonly string DatePromptMessage = + $"Enter the date ({Validation.DateFormat}) of the {{0}}, nothing for right now"; + + public static UserOption.MainMenu MainMenuPrompt() + { + return AnsiConsole.Prompt( + new SelectionPrompt() + .Title(MainMenuTitle) + .PageSize(Enum.GetNames().Length) + .AddChoices(Enum.GetValues()) + .UseConverter(UserOption.MainMenuToString)); + } + + public static UserOption.ViewMenu ViewMenuPrompt() + { + return AnsiConsole.Prompt( + new SelectionPrompt() + .Title(ViewMenuTitle) + .PageSize(Enum.GetNames().Length) + .AddChoices(Enum.GetValues()) + .UseConverter(UserOption.ViewMenuToString)); + } + + public static bool ConfirmationPrompt(string message) + { + return AnsiConsole.Confirm(string.Format(ConfirmationBaseMessage, message)); + } + + public static DateTime AskDate(string message) + { + return AnsiConsole.Prompt( + new TextPrompt(string.Format(DatePromptMessage, message)) + .DefaultValue(DateTime.Now) + .WithConverter(Validation.DateToString) + .ValidationErrorMessage(Validation.DateErrorMessage)); + } +} \ No newline at end of file diff --git a/CodingTracker/UserOption.cs b/CodingTracker/UserOption.cs new file mode 100644 index 000000000..44132ea9b --- /dev/null +++ b/CodingTracker/UserOption.cs @@ -0,0 +1,42 @@ +namespace CodingTracker; + +public static class UserOption +{ + public enum MainMenu + { + Insert, + InsertLive, + View, + Quit + } + + public enum ViewMenu + { + Previous, + Next, + Quit + } + + public static string MainMenuToString(MainMenu option) + { + return option switch + { + MainMenu.Insert => "Insert a new coding session", + MainMenu.InsertLive => "Start a live recording session", + MainMenu.View => "View other months in the calendar", + MainMenu.Quit => "Quit the app", + _ => option.ToString() + }; + } + + public static string ViewMenuToString(ViewMenu option) + { + return option switch + { + ViewMenu.Previous => "Previous month", + ViewMenu.Next => "Next month", + ViewMenu.Quit => "Go back to the menu", + _ => option.ToString() + }; + } +} \ No newline at end of file diff --git a/CodingTracker/Validation.cs b/CodingTracker/Validation.cs new file mode 100644 index 000000000..5c7cd54df --- /dev/null +++ b/CodingTracker/Validation.cs @@ -0,0 +1,13 @@ +namespace CodingTracker; + +public static class Validation +{ + public const string DateFormat = "yyyy-MM-dd: HH-mm"; + public const string DateErrorMessage = "Invalid date format !"; + + public static string DateToString(DateTime time) + { + return time.ToString(DateFormat); + } + +} \ No newline at end of file diff --git a/CodingTrackerTests/ValidationTests.cs b/CodingTrackerTests/ValidationTests.cs new file mode 100644 index 000000000..9ff42536f --- /dev/null +++ b/CodingTrackerTests/ValidationTests.cs @@ -0,0 +1,27 @@ +using CodingTracker; + +namespace CodingTrackerTests; + +public class ValidationTests +{ + [Test] + public void DateToString_FormatsDateTime_AccordingToDateFormatConstant() + { + var testDate = new DateTime(2026, 10, 9, 14, 30, 0); + string expectedOutput = "2026-10-09: 14-30"; + + string result = Validation.DateToString(testDate); + + Assert.That(result, Is.EqualTo(expectedOutput)); + } + + [Test] + public void DateToString_MatchesExpectedFormatPattern() + { + var testDate = DateTime.Now; + + string result = Validation.DateToString(testDate); + + Assert.That(result, Does.Match(@"^\d{4}-\d{2}-\d{2}: \d{2}-\d{2}$")); + } +} \ No newline at end of file From c876297432c0e1d370839dcbeeac5f260d686835 Mon Sep 17 00:00:00 2001 From: Boudi Date: Fri, 9 Oct 2026 02:12:00 +0200 Subject: [PATCH 09/17] Fix appSettings --- CodingTracker/AppSettings.cs | 2 +- CodingTrackerTests/AppSettingsTests.cs | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/CodingTracker/AppSettings.cs b/CodingTracker/AppSettings.cs index 9176919bb..663fdb1a2 100644 --- a/CodingTracker/AppSettings.cs +++ b/CodingTracker/AppSettings.cs @@ -8,7 +8,7 @@ public class AppSettings { public string DbPath { get; set; } = string.Empty; - private static readonly string FilePath = Path.Combine(AppContext.BaseDirectory, "config.json"); + private static readonly string FilePath = Path.Combine(AppContext.BaseDirectory, "appsettings.json"); public static AppSettings LoadConfig() { diff --git a/CodingTrackerTests/AppSettingsTests.cs b/CodingTrackerTests/AppSettingsTests.cs index 210ea2c74..a1fd7d864 100644 --- a/CodingTrackerTests/AppSettingsTests.cs +++ b/CodingTrackerTests/AppSettingsTests.cs @@ -15,6 +15,6 @@ public void Setup() public void Test1() { string expectedDbPath = "CodingTracker.db"; - Assert.Equals(validAppSettings.DbPath, expectedDbPath); + Assert.That(validAppSettings.DbPath, Is.EqualTo(expectedDbPath)); } } \ No newline at end of file From fcf7f206aed03628a040593f5bed98f2c69574c1 Mon Sep 17 00:00:00 2001 From: Boudi Date: Sat, 10 Oct 2026 19:41:09 +0200 Subject: [PATCH 10/17] Create calendar to visualize coding sessions --- CodingTracker/CodingSession.cs | 16 +++------- CodingTracker/TableVisualisationEngine.cs | 39 +++++++++++++++++++++++ 2 files changed, 43 insertions(+), 12 deletions(-) create mode 100644 CodingTracker/TableVisualisationEngine.cs diff --git a/CodingTracker/CodingSession.cs b/CodingTracker/CodingSession.cs index db1459c23..e16bb71c9 100644 --- a/CodingTracker/CodingSession.cs +++ b/CodingTracker/CodingSession.cs @@ -5,22 +5,14 @@ namespace CodingTracker; public class CodingSession { - private DateTime StartTime { get; set; } - private DateTime EndTime { get; set; } - private TimeSpan Duration { get; set; } + public DateTime StartTime { get; set; } + private DateTime? EndTime { get; set; } + public TimeSpan Duration { get; set; } - public CodingSession(DateTime startTime, DateTime endTime, TimeSpan duration) + public CodingSession(DateTime startTime, DateTime? endTime, TimeSpan duration) { StartTime = startTime; EndTime = endTime; Duration = duration; } - - public void AddRowToTable(Table table) - { - table.AddRow( - StartTime.ToString(CultureInfo.CurrentCulture), - EndTime.ToString(CultureInfo.CurrentCulture), - Duration.ToString()); - } } \ No newline at end of file diff --git a/CodingTracker/TableVisualisationEngine.cs b/CodingTracker/TableVisualisationEngine.cs new file mode 100644 index 000000000..3a4edd628 --- /dev/null +++ b/CodingTracker/TableVisualisationEngine.cs @@ -0,0 +1,39 @@ +using Spectre.Console; + +namespace CodingTracker; + +public record ColorLevel(double MaxHours, Color Color); + +public static class TableVisualisationEngine +{ + public static readonly ColorLevel[] ColorLevels = + [ + new(1, Color.Grey), + new(3, Color.IndianRed), + new(6, Color.Red), + new(9, Color.Red3), + new(double.MaxValue, Color.DarkRed) + ]; + + public static Calendar GetCalendar(CodingSession[] sessions) + { + if (sessions.Length == 0) throw new Exception("No calendar"); + + int year = sessions[0].StartTime.Year; + int month = sessions[0].StartTime.Month; + var calendar = new Calendar(year, month); + + foreach (var session in sessions) + { + calendar.AddCalendarEvent(year, month, session.StartTime.Day, GetStyle(session.Duration)); + } + return calendar; + } + + private static Style GetStyle(TimeSpan duration) + { + return new Style(ColorLevels.First(level => duration.TotalHours < level.MaxHours).Color); + } + + +} \ No newline at end of file From ad1fd104dafa10963d7d681b09d9f29507529c36 Mon Sep 17 00:00:00 2001 From: Boudi Date: Sun, 11 Oct 2026 23:56:38 +0200 Subject: [PATCH 11/17] Show calendar --- CodingTracker/CodingSession.cs | 11 ++++--- CodingTracker/Controller.cs | 39 +++++++++++++++++++++-- CodingTracker/DataBaseInterface.cs | 22 ++++++++----- CodingTracker/TableVisualisationEngine.cs | 37 ++++++++++++++++++--- CodingTracker/UserInterface.cs | 7 ++++ 5 files changed, 97 insertions(+), 19 deletions(-) diff --git a/CodingTracker/CodingSession.cs b/CodingTracker/CodingSession.cs index e16bb71c9..b82bf0967 100644 --- a/CodingTracker/CodingSession.cs +++ b/CodingTracker/CodingSession.cs @@ -1,16 +1,17 @@ -using System.Globalization; -using Spectre.Console; - namespace CodingTracker; public class CodingSession { + public long? Id { get; set; } public DateTime StartTime { get; set; } - private DateTime? EndTime { get; set; } + public DateTime? EndTime { get; set; } public TimeSpan Duration { get; set; } - public CodingSession(DateTime startTime, DateTime? endTime, TimeSpan duration) + public CodingSession() { } + + public CodingSession(DateTime startTime, DateTime? endTime, TimeSpan duration, long? id = null) { + Id = id; StartTime = startTime; EndTime = endTime; Duration = duration; diff --git a/CodingTracker/Controller.cs b/CodingTracker/Controller.cs index 50eacb697..0b0900af5 100644 --- a/CodingTracker/Controller.cs +++ b/CodingTracker/Controller.cs @@ -2,11 +2,22 @@ namespace CodingTracker; public static class Controller { + private static DateTime _monthToShow = GetFirstDayMonth(DateTime.Now); + public static async Task Start() { - Console.WriteLine("Hello, World!"); - } + bool endApp = false; + UserInterface.Title(); + do + { + var sessions = await DataBaseInterface.ViewCodingSessions(_monthToShow, NextMonth(_monthToShow)); + var sessionsPerDay = AmountCodingPerDay(sessions); + + TableVisualisationEngine.ShowCalendarWithCaption(sessionsPerDay); + + } while (!endApp); + } public static int Duration(DateTime start, DateTime end) { @@ -14,5 +25,29 @@ public static int Duration(DateTime start, DateTime end) return (end - start).Seconds; } + + public static DateTime GetFirstDayMonth(DateTime time) + { + return new DateTime(time.Date.Year, time.Date.Month, 1); + } + + public static DateTime NextMonth(DateTime time) + { + return time.AddMonths(1); + } + + /// Aggregates coding sessions by calendar day, returning one session per day with summed durations + /// We take this postulate: Sessions that happens past midnight are attributed entirely to their start date + public static List AmountCodingPerDay(List codingSessions) + { + return codingSessions + .GroupBy(codingSession => codingSession.StartTime.Date) // Only take the day of the startTime + .Select(codingSession => + new CodingSession( + codingSession.Key, + codingSession.Key.AddDays(1), + TimeSpan.FromTicks(codingSession.Sum(s => s.Duration.Ticks)))) + .ToList(); + } } \ No newline at end of file diff --git a/CodingTracker/DataBaseInterface.cs b/CodingTracker/DataBaseInterface.cs index a49668da6..29a0b3600 100644 --- a/CodingTracker/DataBaseInterface.cs +++ b/CodingTracker/DataBaseInterface.cs @@ -5,40 +5,46 @@ namespace CodingTracker; public static class DataBaseInterface { - private static readonly SqliteConnection DbConnection; + private static SqliteConnection? _dbConnection; + private static bool _isInitialized = false; - static DataBaseInterface() + private static void InitializeDb() { + if (_isInitialized && _dbConnection != null) return; string dbPath = ReadConfig.Config.DbPath; - DbConnection = new SqliteConnection(dbPath); - DbConnection.Open(); + string connectionString = "Data Source=" + dbPath; + _dbConnection = new SqliteConnection(connectionString); + _dbConnection.Open(); CreateTable(); + _isInitialized = true; } private static void CreateTable() { string createTableQuery = @" CREATE TABLE IF NOT EXISTS CodingSessions ( - Id INT PRIMARY KEY AUTOINCREMENT, + Id INTEGER PRIMARY KEY AUTOINCREMENT, StartTime TEXT NOT NULL, EndTime TEXT NOT NULL, Duration INT NOT NULL);"; - using var command = new SqliteCommand(createTableQuery, DbConnection); + using var command = new SqliteCommand(createTableQuery, _dbConnection); command.ExecuteNonQuery(); } public static async Task AddCodingSession(CodingSession codingSession) { + InitializeDb(); string addCodingSessionQuery = @" INSERT INTO CodingSessions (StartTime, EndTime, Duration) VALUES (@StartTime, @EndTime, @Duration);"; - await DbConnection.ExecuteAsync(addCodingSessionQuery, codingSession); + await _dbConnection.ExecuteAsync(addCodingSessionQuery, codingSession); } public static async Task> ViewCodingSessions(DateTime? minStartDate = null, DateTime? maxStartDate = null) { + InitializeDb(); var conditions = new List(); if (minStartDate.HasValue) conditions.Add("StartTime >= @MinDate"); if (maxStartDate.HasValue) conditions.Add("StartTime <= @MaxDate"); @@ -47,7 +53,7 @@ public static async Task> ViewCodingSessions(DateTime? minSt string viewCodingSessionsQuery = $"SELECT * FROM CodingSessions{whereClauses} ORDER BY StartTime;"; - return (await DbConnection.QueryAsync(viewCodingSessionsQuery, + return (await _dbConnection.QueryAsync(viewCodingSessionsQuery, new { MinDate = minStartDate, MaxDate = maxStartDate })).ToList(); } diff --git a/CodingTracker/TableVisualisationEngine.cs b/CodingTracker/TableVisualisationEngine.cs index 3a4edd628..ba538827c 100644 --- a/CodingTracker/TableVisualisationEngine.cs +++ b/CodingTracker/TableVisualisationEngine.cs @@ -6,7 +6,7 @@ public record ColorLevel(double MaxHours, Color Color); public static class TableVisualisationEngine { - public static readonly ColorLevel[] ColorLevels = + private static readonly ColorLevel[] ColorLevels = [ new(1, Color.Grey), new(3, Color.IndianRed), @@ -14,10 +14,20 @@ public static class TableVisualisationEngine new(9, Color.Red3), new(double.MaxValue, Color.DarkRed) ]; + + public static void ShowCalendarWithCaption(List sessions) + { + var caption = GetCaptionForCalendar(); + var alignedCaption = Align.Left(caption, VerticalAlignment.Middle).Height(10); + + var grid = new Grid().AddColumn().AddColumn(); + grid.AddRow(GetCalendar(sessions), alignedCaption); + AnsiConsole.Live(grid).Start(ctx => ctx.Refresh()); + } - public static Calendar GetCalendar(CodingSession[] sessions) + private static Calendar GetCalendar(List sessions) { - if (sessions.Length == 0) throw new Exception("No calendar"); + if (sessions.Count == 0) return new Calendar(DateTime.Today.Year, DateTime.Today.Month); int year = sessions[0].StartTime.Year; int month = sessions[0].StartTime.Month; @@ -35,5 +45,24 @@ private static Style GetStyle(TimeSpan duration) return new Style(ColorLevels.First(level => duration.TotalHours < level.MaxHours).Color); } - + private static Grid GetCaptionForCalendar() + { + var captionTable = new Grid() + .AddColumn() + .AddColumn(); + + double previousMax = 0; + + foreach (var colorLevel in ColorLevels) + { + string rangeString = $"{previousMax}h " + + (colorLevel.MaxHours > 24 ? $"or more" : $"to {colorLevel.MaxHours}h"); + string coloredShape = $"[{colorLevel.Color}]██[/]"; + captionTable.AddRow(coloredShape, rangeString); + + previousMax = colorLevel.MaxHours; + } + + return captionTable; + } } \ No newline at end of file diff --git a/CodingTracker/UserInterface.cs b/CodingTracker/UserInterface.cs index d72d04a70..418493906 100644 --- a/CodingTracker/UserInterface.cs +++ b/CodingTracker/UserInterface.cs @@ -5,6 +5,7 @@ namespace CodingTracker; public static class UserInterface { private const string TitleStyle = "[green]"; + private const string TitleString = "Coding Session"; private const string MainMenuTitle = $"{TitleStyle}What do you want to do ?[/]"; private const string ViewMenuTitle = $"{TitleStyle}Navigate through the months[/]"; @@ -13,6 +14,12 @@ public static class UserInterface private static readonly string DatePromptMessage = $"Enter the date ({Validation.DateFormat}) of the {{0}}, nothing for right now"; + public static void Title() + { + var title = new Text(TitleString, new Style(foreground: Color.Green1)); + var alignedTitle = Align.Center(title); + AnsiConsole.Write(alignedTitle); + } public static UserOption.MainMenu MainMenuPrompt() { return AnsiConsole.Prompt( From 008b89056f3b3cd8bd076d10c1bf4af0f78b681d Mon Sep 17 00:00:00 2001 From: Boudi Date: Mon, 12 Oct 2026 02:09:52 +0200 Subject: [PATCH 12/17] Menu logic --- CodingTracker/CodingSession.cs | 16 +++- CodingTracker/Controller.cs | 92 +++++++++++++++++++++-- CodingTracker/DataBaseInterface.cs | 6 +- CodingTracker/TableVisualisationEngine.cs | 2 +- CodingTracker/UserInterface.cs | 48 ++++++++++-- CodingTracker/UserOption.cs | 11 +++ CodingTracker/Validation.cs | 9 +++ 7 files changed, 166 insertions(+), 18 deletions(-) diff --git a/CodingTracker/CodingSession.cs b/CodingTracker/CodingSession.cs index b82bf0967..9314d72f5 100644 --- a/CodingTracker/CodingSession.cs +++ b/CodingTracker/CodingSession.cs @@ -1,3 +1,5 @@ +using System.ComponentModel.DataAnnotations.Schema; + namespace CodingTracker; public class CodingSession @@ -5,7 +7,14 @@ public class CodingSession public long? Id { get; set; } public DateTime StartTime { get; set; } public DateTime? EndTime { get; set; } - public TimeSpan Duration { get; set; } + public long? DurationTicks { get; set; } + // We use Duration to treat with TimeSpan in the code even if SQLite doesn't support it + [NotMapped] + public TimeSpan Duration + { + get => TimeSpan.FromTicks(DurationTicks ?? 0); + set => DurationTicks = value.Ticks; + } public CodingSession() { } @@ -16,4 +25,9 @@ public CodingSession(DateTime startTime, DateTime? endTime, TimeSpan duration, l EndTime = endTime; Duration = duration; } + + public override string ToString() + { + return $@"Coding Session {{start: {StartTime}, end: {EndTime}, duration: {Duration} }}"; + } } \ No newline at end of file diff --git a/CodingTracker/Controller.cs b/CodingTracker/Controller.cs index 0b0900af5..ee1c86d24 100644 --- a/CodingTracker/Controller.cs +++ b/CodingTracker/Controller.cs @@ -2,28 +2,104 @@ namespace CodingTracker; public static class Controller { - private static DateTime _monthToShow = GetFirstDayMonth(DateTime.Now); + public static DateTime MonthToShow = GetFirstDayMonth(DateTime.Now); + private static UserOption.PromptMenus _promptMenu = UserOption.PromptMenus.Main; + private static bool _endApp = false; public static async Task Start() { - bool endApp = false; - UserInterface.Title(); do { - var sessions = await DataBaseInterface.ViewCodingSessions(_monthToShow, NextMonth(_monthToShow)); + var sessions = await DataBaseInterface.ViewCodingSessions(MonthToShow, NextMonth(MonthToShow)); var sessionsPerDay = AmountCodingPerDay(sessions); TableVisualisationEngine.ShowCalendarWithCaption(sessionsPerDay); - - } while (!endApp); + await Prompt(); + + } while (!_endApp); + } + + private static async Task Prompt() + { + switch (_promptMenu) + { + case UserOption.PromptMenus.Main: + var choiceMainMenu = UserInterface.MainMenuPrompt(); + MainMenu(choiceMainMenu); + break; + case UserOption.PromptMenus.View: + var choiceViewMenu = UserInterface.ViewMenuPrompt(); + ViewMenu(choiceViewMenu); + break; + case UserOption.PromptMenus.AddLive: + var (startDateLive, endDateLive) = UserInterface.AskDateRangeLive(); + await AddMenu(startDateLive, endDateLive); + _promptMenu = UserOption.PromptMenus.Main; + break; + case UserOption.PromptMenus.Add: + var (startDate, endDate) = UserInterface.AskDateRange(); + await AddMenu(startDate, endDate); + _promptMenu = UserOption.PromptMenus.Main; + break; + default: + throw new ArgumentOutOfRangeException("Not valid promptMenu"); + } + } + + private static void MainMenu(UserOption.MainMenu choice) + { + switch (choice) + { + case UserOption.MainMenu.Insert: + _promptMenu = UserOption.PromptMenus.Add; + break; + case UserOption.MainMenu.InsertLive: + _promptMenu = UserOption.PromptMenus.AddLive; + break; + case UserOption.MainMenu.View: + _promptMenu = UserOption.PromptMenus.View; + break; + case UserOption.MainMenu.Quit: + _endApp = true; + break; + default: + throw new ArgumentOutOfRangeException(nameof(choice), choice, null); + } + } + + private static void ViewMenu(UserOption.ViewMenu choice) + { + switch (choice) + { + case UserOption.ViewMenu.Previous: + MonthToShow = MonthToShow.AddMonths(-1); + break; + case UserOption.ViewMenu.Next: + MonthToShow = MonthToShow.AddMonths(1); + break; + case UserOption.ViewMenu.Quit: + _promptMenu = UserOption.PromptMenus.Main; + break; + default: + throw new ArgumentOutOfRangeException(nameof(choice), choice, null); + } + } + + private static async Task AddMenu(DateTime start, DateTime end) + { + var duration = Duration(start, end); + var codingSession = new CodingSession(start, end, duration); + bool confirmation = UserInterface.ConfirmationPrompt($"add {codingSession}"); + + if (confirmation) await DataBaseInterface.AddCodingSession(codingSession); } - public static int Duration(DateTime start, DateTime end) + public static TimeSpan Duration(DateTime start, DateTime end) { if (start.CompareTo(end) >= 0) throw new Exception("Start must be earlier than end"); - return (end - start).Seconds; + return (end - start); } public static DateTime GetFirstDayMonth(DateTime time) diff --git a/CodingTracker/DataBaseInterface.cs b/CodingTracker/DataBaseInterface.cs index 29a0b3600..9f3bd4472 100644 --- a/CodingTracker/DataBaseInterface.cs +++ b/CodingTracker/DataBaseInterface.cs @@ -26,7 +26,7 @@ CREATE TABLE IF NOT EXISTS CodingSessions ( Id INTEGER PRIMARY KEY AUTOINCREMENT, StartTime TEXT NOT NULL, EndTime TEXT NOT NULL, - Duration INT NOT NULL);"; + DurationTicks INTEGER NOT NULL);"; using var command = new SqliteCommand(createTableQuery, _dbConnection); command.ExecuteNonQuery(); @@ -36,8 +36,8 @@ public static async Task AddCodingSession(CodingSession codingSession) { InitializeDb(); string addCodingSessionQuery = @" - INSERT INTO CodingSessions (StartTime, EndTime, Duration) - VALUES (@StartTime, @EndTime, @Duration);"; + INSERT INTO CodingSessions (StartTime, EndTime, DurationTicks) + VALUES (@StartTime, @EndTime, @DurationTicks);"; await _dbConnection.ExecuteAsync(addCodingSessionQuery, codingSession); } diff --git a/CodingTracker/TableVisualisationEngine.cs b/CodingTracker/TableVisualisationEngine.cs index ba538827c..7717a29a8 100644 --- a/CodingTracker/TableVisualisationEngine.cs +++ b/CodingTracker/TableVisualisationEngine.cs @@ -27,7 +27,7 @@ public static void ShowCalendarWithCaption(List sessions) private static Calendar GetCalendar(List sessions) { - if (sessions.Count == 0) return new Calendar(DateTime.Today.Year, DateTime.Today.Month); + if (sessions.Count == 0) return new Calendar(Controller.MonthToShow.Year, Controller.MonthToShow.Month); int year = sessions[0].StartTime.Year; int month = sessions[0].StartTime.Month; diff --git a/CodingTracker/UserInterface.cs b/CodingTracker/UserInterface.cs index 418493906..0b48db51b 100644 --- a/CodingTracker/UserInterface.cs +++ b/CodingTracker/UserInterface.cs @@ -14,6 +14,12 @@ public static class UserInterface private static readonly string DatePromptMessage = $"Enter the date ({Validation.DateFormat}) of the {{0}}, nothing for right now"; + private static readonly string DatePromptLiveMessage = + $"You started the live session at {{0}}, press enter to end the session"; + + private static readonly string DatePromptErrorEndBeforeStart = + "[red]End date cannot be earlier than start date. Try again.[/]"; + public static void Title() { var title = new Text(TitleString, new Style(foreground: Color.Green1)); @@ -40,17 +46,49 @@ public static UserOption.ViewMenu ViewMenuPrompt() .UseConverter(UserOption.ViewMenuToString)); } + public static (DateTime, DateTime) AskDateRange() + { + + var startDate = AskDate("start date"); + var endDate = AskDate("end date"); + + while (endDate <= startDate) + { + AnsiConsole.MarkupLine(DatePromptErrorEndBeforeStart); + endDate = AskDate("end date"); + } + + return (startDate, endDate); + } + + public static (DateTime, DateTime) AskDateRangeLive() + { + var startDate = DateTime.Now; + AnsiConsole.MarkupLine(string.Format(DatePromptLiveMessage, startDate)); + + // Wait user input without showing it in console + Console.ReadKey(intercept: true); + + var endDate = DateTime.Now; + return (startDate, endDate); + } + public static bool ConfirmationPrompt(string message) { return AnsiConsole.Confirm(string.Format(ConfirmationBaseMessage, message)); } - public static DateTime AskDate(string message) + private static DateTime AskDate(string message) { - return AnsiConsole.Prompt( - new TextPrompt(string.Format(DatePromptMessage, message)) - .DefaultValue(DateTime.Now) - .WithConverter(Validation.DateToString) + string formattedMessage = string.Format(DatePromptMessage, message); + string defaultValue = Validation.DateToString(DateTime.Now); + Validation.DateTryParser(defaultValue, out DateTime parsedDate); + + AnsiConsole.Prompt( + new TextPrompt(formattedMessage) + .DefaultValue(defaultValue) + .Validate(input => Validation.DateTryParser(input, out parsedDate)) .ValidationErrorMessage(Validation.DateErrorMessage)); + return parsedDate; } } \ No newline at end of file diff --git a/CodingTracker/UserOption.cs b/CodingTracker/UserOption.cs index 44132ea9b..586245beb 100644 --- a/CodingTracker/UserOption.cs +++ b/CodingTracker/UserOption.cs @@ -2,6 +2,7 @@ namespace CodingTracker; public static class UserOption { + // Option for the main menu selection prompt public enum MainMenu { Insert, @@ -10,12 +11,22 @@ public enum MainMenu Quit } + // Option for the view menu selection prompt public enum ViewMenu { Previous, Next, Quit } + + // Different prompt that can be shown + public enum PromptMenus + { + Main, + Add, + AddLive, + View + } public static string MainMenuToString(MainMenu option) { diff --git a/CodingTracker/Validation.cs b/CodingTracker/Validation.cs index 5c7cd54df..0704889b7 100644 --- a/CodingTracker/Validation.cs +++ b/CodingTracker/Validation.cs @@ -1,3 +1,6 @@ +using System.ComponentModel.DataAnnotations; +using System.Globalization; + namespace CodingTracker; public static class Validation @@ -9,5 +12,11 @@ public static string DateToString(DateTime time) { return time.ToString(DateFormat); } + + public static bool DateTryParser(string input, out DateTime parsedDate) + { + return (DateTime.TryParseExact(input?.Trim(), DateFormat, CultureInfo.InvariantCulture, DateTimeStyles.None, + out parsedDate)); + } } \ No newline at end of file From 070e20b16e7bdbcf0a79231d0294ef5443f7dc0b Mon Sep 17 00:00:00 2001 From: Boudi Date: Mon, 12 Oct 2026 02:12:00 +0200 Subject: [PATCH 13/17] refactor into utils class --- CodingTracker/Controller.cs | 23 +++-------------------- CodingTracker/Utils.cs | 22 ++++++++++++++++++++++ 2 files changed, 25 insertions(+), 20 deletions(-) create mode 100644 CodingTracker/Utils.cs diff --git a/CodingTracker/Controller.cs b/CodingTracker/Controller.cs index ee1c86d24..5c0f7ccdd 100644 --- a/CodingTracker/Controller.cs +++ b/CodingTracker/Controller.cs @@ -2,7 +2,7 @@ namespace CodingTracker; public static class Controller { - public static DateTime MonthToShow = GetFirstDayMonth(DateTime.Now); + public static DateTime MonthToShow = Utils.GetFirstDayMonth(DateTime.Now); private static UserOption.PromptMenus _promptMenu = UserOption.PromptMenus.Main; private static bool _endApp = false; @@ -11,7 +11,7 @@ public static async Task Start() UserInterface.Title(); do { - var sessions = await DataBaseInterface.ViewCodingSessions(MonthToShow, NextMonth(MonthToShow)); + var sessions = await DataBaseInterface.ViewCodingSessions(MonthToShow, Utils.NextMonth(MonthToShow)); var sessionsPerDay = AmountCodingPerDay(sessions); TableVisualisationEngine.ShowCalendarWithCaption(sessionsPerDay); @@ -88,29 +88,12 @@ private static void ViewMenu(UserOption.ViewMenu choice) private static async Task AddMenu(DateTime start, DateTime end) { - var duration = Duration(start, end); + var duration = Utils.Duration(start, end); var codingSession = new CodingSession(start, end, duration); bool confirmation = UserInterface.ConfirmationPrompt($"add {codingSession}"); if (confirmation) await DataBaseInterface.AddCodingSession(codingSession); } - - public static TimeSpan Duration(DateTime start, DateTime end) - { - if (start.CompareTo(end) >= 0) throw new Exception("Start must be earlier than end"); - - return (end - start); - } - - public static DateTime GetFirstDayMonth(DateTime time) - { - return new DateTime(time.Date.Year, time.Date.Month, 1); - } - - public static DateTime NextMonth(DateTime time) - { - return time.AddMonths(1); - } /// Aggregates coding sessions by calendar day, returning one session per day with summed durations /// We take this postulate: Sessions that happens past midnight are attributed entirely to their start date diff --git a/CodingTracker/Utils.cs b/CodingTracker/Utils.cs new file mode 100644 index 000000000..da12a6ebf --- /dev/null +++ b/CodingTracker/Utils.cs @@ -0,0 +1,22 @@ +namespace CodingTracker; + +public static class Utils +{ + + public static TimeSpan Duration(DateTime start, DateTime end) + { + if (start.CompareTo(end) >= 0) throw new Exception("Start must be earlier than end"); + + return (end - start); + } + + public static DateTime GetFirstDayMonth(DateTime time) + { + return new DateTime(time.Date.Year, time.Date.Month, 1); + } + + public static DateTime NextMonth(DateTime time) + { + return time.AddMonths(1); + } +} \ No newline at end of file From 07871bd49068de411be9b9967876a75ce9e004e8 Mon Sep 17 00:00:00 2001 From: Boudi Date: Mon, 12 Oct 2026 02:45:58 +0200 Subject: [PATCH 14/17] Make it more beautiful + add previous month --- CodingTracker/Controller.cs | 20 ++++++++++++++++---- CodingTracker/TableVisualisationEngine.cs | 21 +++++++++++++++------ CodingTracker/UserInterface.cs | 6 ++++-- CodingTracker/Utils.cs | 7 ++++++- 4 files changed, 41 insertions(+), 13 deletions(-) diff --git a/CodingTracker/Controller.cs b/CodingTracker/Controller.cs index 5c0f7ccdd..1ec03a6f3 100644 --- a/CodingTracker/Controller.cs +++ b/CodingTracker/Controller.cs @@ -8,13 +8,19 @@ public static class Controller public static async Task Start() { - UserInterface.Title(); do { var sessions = await DataBaseInterface.ViewCodingSessions(MonthToShow, Utils.NextMonth(MonthToShow)); var sessionsPerDay = AmountCodingPerDay(sessions); + sessionsPerDay = AddEmptyCodingSessionIfEmpty(sessionsPerDay, MonthToShow); + + var previousMonth = Utils.PreviousMonth(MonthToShow); + var sessionsPrevious = await DataBaseInterface.ViewCodingSessions(previousMonth, MonthToShow); + var sessionsPreviousPerDay = AmountCodingPerDay(sessionsPrevious); + sessionsPreviousPerDay = AddEmptyCodingSessionIfEmpty(sessionsPreviousPerDay, previousMonth); - TableVisualisationEngine.ShowCalendarWithCaption(sessionsPerDay); + UserInterface.ClearAndTitle(); + TableVisualisationEngine.ShowCalendarWithCaption(sessionsPreviousPerDay, sessionsPerDay); await Prompt(); } while (!_endApp); @@ -73,10 +79,10 @@ private static void ViewMenu(UserOption.ViewMenu choice) switch (choice) { case UserOption.ViewMenu.Previous: - MonthToShow = MonthToShow.AddMonths(-1); + MonthToShow = Utils.PreviousMonth(MonthToShow); break; case UserOption.ViewMenu.Next: - MonthToShow = MonthToShow.AddMonths(1); + MonthToShow = Utils.NextMonth(MonthToShow); break; case UserOption.ViewMenu.Quit: _promptMenu = UserOption.PromptMenus.Main; @@ -108,5 +114,11 @@ public static List AmountCodingPerDay(List codingS TimeSpan.FromTicks(codingSession.Sum(s => s.Duration.Ticks)))) .ToList(); } + + private static List AddEmptyCodingSessionIfEmpty(List sessions, DateTime monthToShow) + { + if (sessions.Count > 0) return sessions; + return [new CodingSession(monthToShow, monthToShow, new TimeSpan(0))]; + } } \ No newline at end of file diff --git a/CodingTracker/TableVisualisationEngine.cs b/CodingTracker/TableVisualisationEngine.cs index 7717a29a8..65320fde3 100644 --- a/CodingTracker/TableVisualisationEngine.cs +++ b/CodingTracker/TableVisualisationEngine.cs @@ -1,4 +1,6 @@ +using System.Globalization; using Spectre.Console; +using Calendar = Spectre.Console.Calendar; namespace CodingTracker; @@ -15,14 +17,15 @@ public static class TableVisualisationEngine new(double.MaxValue, Color.DarkRed) ]; - public static void ShowCalendarWithCaption(List sessions) + public static void ShowCalendarWithCaption(List sessionsPrevious, List sessions) { var caption = GetCaptionForCalendar(); var alignedCaption = Align.Left(caption, VerticalAlignment.Middle).Height(10); - var grid = new Grid().AddColumn().AddColumn(); - grid.AddRow(GetCalendar(sessions), alignedCaption); - AnsiConsole.Live(grid).Start(ctx => ctx.Refresh()); + var grid = new Grid().AddColumn().AddColumn().AddColumn(); + grid.AddRow(GetCalendar(sessionsPrevious), GetCalendar(sessions), alignedCaption); + + AnsiConsole.Write(grid); } private static Calendar GetCalendar(List sessions) @@ -31,10 +34,15 @@ private static Calendar GetCalendar(List sessions) int year = sessions[0].StartTime.Year; int month = sessions[0].StartTime.Month; - var calendar = new Calendar(year, month); + var calendar = new Calendar(year, month) + .Border(TableBorder.Rounded) // Gives it a nice, smooth border + .HeaderStyle(new Style(foreground: Color.Cyan1, decoration: Decoration.Bold)) + .HighlightStyle(new Style(foreground: Color.White, background: Color.Blue)) + .Culture(CultureInfo.CurrentCulture); foreach (var session in sessions) { + if (session.Duration.Ticks == 0) break; calendar.AddCalendarEvent(year, month, session.StartTime.Day, GetStyle(session.Duration)); } return calendar; @@ -42,7 +50,8 @@ private static Calendar GetCalendar(List sessions) private static Style GetStyle(TimeSpan duration) { - return new Style(ColorLevels.First(level => duration.TotalHours < level.MaxHours).Color); + var color = ColorLevels.First(level => duration.TotalHours < level.MaxHours).Color; + return new Style(background: color); } private static Grid GetCaptionForCalendar() diff --git a/CodingTracker/UserInterface.cs b/CodingTracker/UserInterface.cs index 0b48db51b..4af659c07 100644 --- a/CodingTracker/UserInterface.cs +++ b/CodingTracker/UserInterface.cs @@ -20,10 +20,12 @@ public static class UserInterface private static readonly string DatePromptErrorEndBeforeStart = "[red]End date cannot be earlier than start date. Try again.[/]"; - public static void Title() + public static void ClearAndTitle() { var title = new Text(TitleString, new Style(foreground: Color.Green1)); var alignedTitle = Align.Center(title); + + AnsiConsole.Clear(); AnsiConsole.Write(alignedTitle); } public static UserOption.MainMenu MainMenuPrompt() @@ -52,7 +54,7 @@ public static (DateTime, DateTime) AskDateRange() var startDate = AskDate("start date"); var endDate = AskDate("end date"); - while (endDate <= startDate) + while (endDate < startDate) { AnsiConsole.MarkupLine(DatePromptErrorEndBeforeStart); endDate = AskDate("end date"); diff --git a/CodingTracker/Utils.cs b/CodingTracker/Utils.cs index da12a6ebf..0343953a3 100644 --- a/CodingTracker/Utils.cs +++ b/CodingTracker/Utils.cs @@ -5,7 +5,7 @@ public static class Utils public static TimeSpan Duration(DateTime start, DateTime end) { - if (start.CompareTo(end) >= 0) throw new Exception("Start must be earlier than end"); + if (start.CompareTo(end) > 0) throw new Exception("Start must be earlier than end"); return (end - start); } @@ -15,6 +15,11 @@ public static DateTime GetFirstDayMonth(DateTime time) return new DateTime(time.Date.Year, time.Date.Month, 1); } + public static DateTime PreviousMonth(DateTime time) + { + return time.AddMonths(-1); + } + public static DateTime NextMonth(DateTime time) { return time.AddMonths(1); From 2d346559038d39f1f4d3de3855ce02f16de3d337 Mon Sep 17 00:00:00 2001 From: Boudi Date: Mon, 12 Oct 2026 18:39:32 +0200 Subject: [PATCH 15/17] Update readme --- CodingTracker/CodingSession.cs | 2 +- CodingTracker/DataBaseInterface.cs | 4 ++++ CodingTracker/UserInterface.cs | 2 +- README.md | 8 ++++++++ 4 files changed, 14 insertions(+), 2 deletions(-) diff --git a/CodingTracker/CodingSession.cs b/CodingTracker/CodingSession.cs index 9314d72f5..59467b37c 100644 --- a/CodingTracker/CodingSession.cs +++ b/CodingTracker/CodingSession.cs @@ -8,7 +8,7 @@ public class CodingSession public DateTime StartTime { get; set; } public DateTime? EndTime { get; set; } public long? DurationTicks { get; set; } - // We use Duration to treat with TimeSpan in the code even if SQLite doesn't support it + // We use Duration to use TimeSpan in the code even if SQLite doesn't support it [NotMapped] public TimeSpan Duration { diff --git a/CodingTracker/DataBaseInterface.cs b/CodingTracker/DataBaseInterface.cs index 9f3bd4472..b4e132b69 100644 --- a/CodingTracker/DataBaseInterface.cs +++ b/CodingTracker/DataBaseInterface.cs @@ -35,6 +35,8 @@ CREATE TABLE IF NOT EXISTS CodingSessions ( public static async Task AddCodingSession(CodingSession codingSession) { InitializeDb(); + if (_dbConnection == null) throw new Exception("Database connection couldn't initialize"); + string addCodingSessionQuery = @" INSERT INTO CodingSessions (StartTime, EndTime, DurationTicks) VALUES (@StartTime, @EndTime, @DurationTicks);"; @@ -45,6 +47,8 @@ INSERT INTO CodingSessions (StartTime, EndTime, DurationTicks) public static async Task> ViewCodingSessions(DateTime? minStartDate = null, DateTime? maxStartDate = null) { InitializeDb(); + if (_dbConnection == null) throw new Exception("Database connection couldn't initialize"); + var conditions = new List(); if (minStartDate.HasValue) conditions.Add("StartTime >= @MinDate"); if (maxStartDate.HasValue) conditions.Add("StartTime <= @MaxDate"); diff --git a/CodingTracker/UserInterface.cs b/CodingTracker/UserInterface.cs index 4af659c07..5cdef2e84 100644 --- a/CodingTracker/UserInterface.cs +++ b/CodingTracker/UserInterface.cs @@ -25,7 +25,7 @@ public static void ClearAndTitle() var title = new Text(TitleString, new Style(foreground: Color.Green1)); var alignedTitle = Align.Center(title); - AnsiConsole.Clear(); + //AnsiConsole.Clear(); AnsiConsole.Write(alignedTitle); } public static UserOption.MainMenu MainMenuPrompt() diff --git a/README.md b/README.md index 44af65076..e23144427 100644 --- a/README.md +++ b/README.md @@ -1 +1,9 @@ # Coding Tracker +This is a console app to record your coding sessions \ +And see your activity in a calendar. + +## How to run +In the solution folder of the project run the folowing to execute: \ +`dotnet run --project CodingTracker/CodingTracker.csproj` + +You can personalize the database name in appsettings.json to handle several tracker at the same time \ No newline at end of file From c05905208e800b210827028751a664c84f98e29f Mon Sep 17 00:00:00 2001 From: Boudi Date: Mon, 12 Oct 2026 19:36:06 +0200 Subject: [PATCH 16/17] Unit test --- CodingTrackerTests/CodingSessionTests.cs | 40 +++++++++++++++++ CodingTrackerTests/UtilsTests.cs | 56 ++++++++++++++++++++++++ CodingTrackerTests/ValidationTests.cs | 22 ++++++++++ 3 files changed, 118 insertions(+) create mode 100644 CodingTrackerTests/CodingSessionTests.cs create mode 100644 CodingTrackerTests/UtilsTests.cs diff --git a/CodingTrackerTests/CodingSessionTests.cs b/CodingTrackerTests/CodingSessionTests.cs new file mode 100644 index 000000000..6dc5c2705 --- /dev/null +++ b/CodingTrackerTests/CodingSessionTests.cs @@ -0,0 +1,40 @@ +using CodingTracker; + +namespace CodingTrackerTests; + +public class CodingSessionTests +{ + [Test] + public void DefaultConstructorCheck() + { + var session = new CodingSession(); + + Assert.That(session.Id, Is.Null); + Assert.That(session.StartTime, Is.EqualTo(default(DateTime))); + Assert.That(session.EndTime, Is.Null); + Assert.That(session.DurationTicks, Is.Null); + Assert.That(session.Duration, Is.EqualTo(TimeSpan.Zero)); + } + + [Test] + public void Duration_UpdatesDurationTicks() + { + var session = new CodingSession(); + var duration = TimeSpan.FromMinutes(45); + session.Duration = duration; + + Assert.That(session.DurationTicks, Is.EqualTo(duration.Ticks)); + Assert.That(session.Duration, Is.EqualTo(duration)); + } + + [Test] + public void Duration_ReturnsZero_WhenDurationTicksIsNull() + { + var session = new CodingSession + { + DurationTicks = null + }; + + Assert.That(session.Duration, Is.EqualTo(TimeSpan.Zero)); + } +} \ No newline at end of file diff --git a/CodingTrackerTests/UtilsTests.cs b/CodingTrackerTests/UtilsTests.cs new file mode 100644 index 000000000..bfc37f083 --- /dev/null +++ b/CodingTrackerTests/UtilsTests.cs @@ -0,0 +1,56 @@ +using CodingTracker; + +namespace CodingTrackerTests; + +public class UtilsTests +{ + [Test] + public void Duration_WithValidDates() + { + var start = new DateTime(2026, 10, 12, 10, 0, 0); + var end = new DateTime(2026, 10, 12, 12, 30, 0); + TimeSpan result = Utils.Duration(start, end); + + Assert.That(result, Is.EqualTo(TimeSpan.FromHours(2.5))); + } + + [Test] + public void Duration_ThrowsException() + { + var start = new DateTime(2026, 10, 12, 14, 0, 0); + var end = new DateTime(2026, 10, 12, 12, 0, 0); + + var ex = Assert.Throws(() => Utils.Duration(start, end)); + Assert.That(ex.Message, Is.EqualTo("Start must be earlier than end")); + } + + [Test] + public void GetFirstDayMonth() + { + var date = new DateTime(2026, 10, 12, 15, 45, 30); + DateTime result = Utils.GetFirstDayMonth(date); + + var expected = new DateTime(2026, 10, 1, 0, 0, 0); + Assert.That(result, Is.EqualTo(expected)); + } + + [Test] + public void PreviousMonth() + { + var date = new DateTime(2026, 10, 12); + DateTime result = Utils.PreviousMonth(date); + + var expected = new DateTime(2026, 9, 12); + Assert.That(result, Is.EqualTo(expected)); + } + + [Test] + public void NextMonth() + { + var date = new DateTime(2026, 10, 12); + DateTime result = Utils.NextMonth(date); + + var expected = new DateTime(2026, 11, 12); + Assert.That(result, Is.EqualTo(expected)); + } +} \ No newline at end of file diff --git a/CodingTrackerTests/ValidationTests.cs b/CodingTrackerTests/ValidationTests.cs index 9ff42536f..d8dd321d0 100644 --- a/CodingTrackerTests/ValidationTests.cs +++ b/CodingTrackerTests/ValidationTests.cs @@ -24,4 +24,26 @@ public void DateToString_MatchesExpectedFormatPattern() Assert.That(result, Does.Match(@"^\d{4}-\d{2}-\d{2}: \d{2}-\d{2}$")); } + + + [TestCase("2026-10-12: 18-30", true)] + [TestCase(" 2026-10-12: 18-30 ", true)] + public void DateTryParser_WithValidInput_ReturnsTrueAndParsedDate(string input, bool expectedResult) + { + bool success = Validation.DateTryParser(input, out DateTime parsedDate); + + Assert.That(success, Is.EqualTo(expectedResult)); + Assert.That(parsedDate, Is.Not.EqualTo(default(DateTime))); + } + + [TestCase("12-10-2026")] + [TestCase("not-a-date")] + [TestCase("")] + public void DateTryParser_WithInvalidInput_ReturnsFalse(string input) + { + bool success = Validation.DateTryParser(input, out DateTime parsedDate); + + Assert.That(success, Is.False); + } + } \ No newline at end of file From e4a4e2daf7934aab05ef1f64bc98d2db8d5f6495 Mon Sep 17 00:00:00 2001 From: Boudi Date: Mon, 12 Oct 2026 19:40:50 +0200 Subject: [PATCH 17/17] clean up --- CodingTracker/Controller.cs | 2 +- CodingTracker/UserInterface.cs | 3 +-- CodingTracker/Validation.cs | 2 -- 3 files changed, 2 insertions(+), 5 deletions(-) diff --git a/CodingTracker/Controller.cs b/CodingTracker/Controller.cs index 1ec03a6f3..7b2b9e940 100644 --- a/CodingTracker/Controller.cs +++ b/CodingTracker/Controller.cs @@ -49,7 +49,7 @@ private static async Task Prompt() _promptMenu = UserOption.PromptMenus.Main; break; default: - throw new ArgumentOutOfRangeException("Not valid promptMenu"); + throw new ArgumentOutOfRangeException($"Not valid promptMenu"); } } diff --git a/CodingTracker/UserInterface.cs b/CodingTracker/UserInterface.cs index 5cdef2e84..c3e250a14 100644 --- a/CodingTracker/UserInterface.cs +++ b/CodingTracker/UserInterface.cs @@ -25,7 +25,7 @@ public static void ClearAndTitle() var title = new Text(TitleString, new Style(foreground: Color.Green1)); var alignedTitle = Align.Center(title); - //AnsiConsole.Clear(); + AnsiConsole.Clear(); AnsiConsole.Write(alignedTitle); } public static UserOption.MainMenu MainMenuPrompt() @@ -50,7 +50,6 @@ public static UserOption.ViewMenu ViewMenuPrompt() public static (DateTime, DateTime) AskDateRange() { - var startDate = AskDate("start date"); var endDate = AskDate("end date"); diff --git a/CodingTracker/Validation.cs b/CodingTracker/Validation.cs index 0704889b7..f924c9092 100644 --- a/CodingTracker/Validation.cs +++ b/CodingTracker/Validation.cs @@ -1,4 +1,3 @@ -using System.ComponentModel.DataAnnotations; using System.Globalization; namespace CodingTracker; @@ -18,5 +17,4 @@ public static bool DateTryParser(string input, out DateTime parsedDate) return (DateTime.TryParseExact(input?.Trim(), DateFormat, CultureInfo.InvariantCulture, DateTimeStyles.None, out parsedDate)); } - } \ No newline at end of file