From f7e2d2c2b994c1c192933f72b2745bb705094a1e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Kerem=20Bj=C3=A4lven=C3=A4s=20Tazedal?= <98690898+Kerem1989@users.noreply.github.com> Date: Tue, 4 Aug 2026 17:10:02 +0200 Subject: [PATCH 1/6] Added feature, create coding session --- Kerem.CodingTracker/Kerem.CodingTracker.sln | 16 +++++++ .../Domain/Entities/CodingSession.cs | 9 ++++ .../Interfaces/ICodingSessionRepository.cs | 7 +++ .../CreateCodingSession.cs | 41 ++++++++++++++++ .../CreateCodingSessionValidator.cs | 17 +++++++ .../Persistance/DapperDbContext.cs | 34 +++++++++++++ .../Repositories/CodingSessionRepository.cs | 30 ++++++++++++ .../Kerem.CodingTracker.csproj | 16 +++++++ .../Kerem.CodingTracker/Program.cs | 12 +++++ .../Kerem.CodingTracker/UI/ConsoleMenu.cs | 48 +++++++++++++++++++ Kerem.CodingTracker/global.json | 7 +++ 11 files changed, 237 insertions(+) create mode 100644 Kerem.CodingTracker/Kerem.CodingTracker.sln create mode 100644 Kerem.CodingTracker/Kerem.CodingTracker/Domain/Entities/CodingSession.cs create mode 100644 Kerem.CodingTracker/Kerem.CodingTracker/Domain/Interfaces/ICodingSessionRepository.cs create mode 100644 Kerem.CodingTracker/Kerem.CodingTracker/Features/CreateCodingSession/CreateCodingSession.cs create mode 100644 Kerem.CodingTracker/Kerem.CodingTracker/Features/CreateCodingSession/CreateCodingSessionValidator.cs create mode 100644 Kerem.CodingTracker/Kerem.CodingTracker/Infrastructure/Persistance/DapperDbContext.cs create mode 100644 Kerem.CodingTracker/Kerem.CodingTracker/Infrastructure/Repositories/CodingSessionRepository.cs create mode 100644 Kerem.CodingTracker/Kerem.CodingTracker/Kerem.CodingTracker.csproj create mode 100644 Kerem.CodingTracker/Kerem.CodingTracker/Program.cs create mode 100644 Kerem.CodingTracker/Kerem.CodingTracker/UI/ConsoleMenu.cs create mode 100644 Kerem.CodingTracker/global.json diff --git a/Kerem.CodingTracker/Kerem.CodingTracker.sln b/Kerem.CodingTracker/Kerem.CodingTracker.sln new file mode 100644 index 000000000..3c8a2a0c5 --- /dev/null +++ b/Kerem.CodingTracker/Kerem.CodingTracker.sln @@ -0,0 +1,16 @@ + +Microsoft Visual Studio Solution File, Format Version 12.00 +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Kerem.CodingTracker", "Kerem.CodingTracker\Kerem.CodingTracker.csproj", "{03B58CE3-2E2B-496D-BD1E-DD53FA74B4FE}" +EndProject +Global + GlobalSection(SolutionConfigurationPlatforms) = preSolution + Debug|Any CPU = Debug|Any CPU + Release|Any CPU = Release|Any CPU + EndGlobalSection + GlobalSection(ProjectConfigurationPlatforms) = postSolution + {03B58CE3-2E2B-496D-BD1E-DD53FA74B4FE}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {03B58CE3-2E2B-496D-BD1E-DD53FA74B4FE}.Debug|Any CPU.Build.0 = Debug|Any CPU + {03B58CE3-2E2B-496D-BD1E-DD53FA74B4FE}.Release|Any CPU.ActiveCfg = Release|Any CPU + {03B58CE3-2E2B-496D-BD1E-DD53FA74B4FE}.Release|Any CPU.Build.0 = Release|Any CPU + EndGlobalSection +EndGlobal diff --git a/Kerem.CodingTracker/Kerem.CodingTracker/Domain/Entities/CodingSession.cs b/Kerem.CodingTracker/Kerem.CodingTracker/Domain/Entities/CodingSession.cs new file mode 100644 index 000000000..9917cae5b --- /dev/null +++ b/Kerem.CodingTracker/Kerem.CodingTracker/Domain/Entities/CodingSession.cs @@ -0,0 +1,9 @@ +namespace Kerem.CodingTracker ; + + public class CodingSession + { + public int id { get; set; } + public DateTime startTime { get; set; } + public DateTime endTime { get; set; } + public Decimal duration { get; set; } + } \ No newline at end of file diff --git a/Kerem.CodingTracker/Kerem.CodingTracker/Domain/Interfaces/ICodingSessionRepository.cs b/Kerem.CodingTracker/Kerem.CodingTracker/Domain/Interfaces/ICodingSessionRepository.cs new file mode 100644 index 000000000..3952d6089 --- /dev/null +++ b/Kerem.CodingTracker/Kerem.CodingTracker/Domain/Interfaces/ICodingSessionRepository.cs @@ -0,0 +1,7 @@ +namespace Kerem.CodingTracker.Domain.Interfaces ; + + public interface ICodingSessionRepository + { + List FindAll(); + void Create(CodingSession codingSession); + } \ No newline at end of file diff --git a/Kerem.CodingTracker/Kerem.CodingTracker/Features/CreateCodingSession/CreateCodingSession.cs b/Kerem.CodingTracker/Kerem.CodingTracker/Features/CreateCodingSession/CreateCodingSession.cs new file mode 100644 index 000000000..bd306898e --- /dev/null +++ b/Kerem.CodingTracker/Kerem.CodingTracker/Features/CreateCodingSession/CreateCodingSession.cs @@ -0,0 +1,41 @@ +using System.Data; +using Kerem.CodingTracker.Domain.Interfaces; +using Kerem.CodingTracker.Infrastructure.Repositories; + +namespace Kerem.CodingTracker.Features.CreateCodingSession ; + + public class CreateCodingSession + { + private readonly ICodingSessionRepository _codingSessionRepository; + + public CreateCodingSession(ICodingSessionRepository codingSessionRepository) + { + _codingSessionRepository = codingSessionRepository; + } + + public void Create() + { + CodingSession codingSession = new CodingSession(); + Console.WriteLine("Please enter the start date"); + + var startDate = Console.ReadLine(); + var correctFomat = CreateCodingSessionValidator.ValidateDateFormat(startDate); + + if (!correctFomat) + { + Console.WriteLine("Format is invalid"); + return; + } + + DateTime startTime = DateTime.Parse(startDate); + codingSession.startTime = startTime; + + Console.WriteLine("Please enter the end date"); + var endDate = Console.ReadLine(); + DateTime endTime = DateTime.Parse(endDate); + codingSession.endTime = endTime; + decimal durationInSeconds = Convert.ToDecimal(DateTime.Now.Subtract(startTime).TotalSeconds); + codingSession.duration = 0; + _codingSessionRepository.Create(codingSession); + } + } \ No newline at end of file diff --git a/Kerem.CodingTracker/Kerem.CodingTracker/Features/CreateCodingSession/CreateCodingSessionValidator.cs b/Kerem.CodingTracker/Kerem.CodingTracker/Features/CreateCodingSession/CreateCodingSessionValidator.cs new file mode 100644 index 000000000..c7e87372f --- /dev/null +++ b/Kerem.CodingTracker/Kerem.CodingTracker/Features/CreateCodingSession/CreateCodingSessionValidator.cs @@ -0,0 +1,17 @@ +using System.Text.RegularExpressions; + +namespace Kerem.CodingTracker.Features.CreateCodingSession ; + + public static class CreateCodingSessionValidator + { + public static bool ValidateDateFormat(String date) + { + string pattern = @"^\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2}$"; + if (Regex.IsMatch(date, pattern)) + { + return true; + } + return false; + } + + } \ No newline at end of file diff --git a/Kerem.CodingTracker/Kerem.CodingTracker/Infrastructure/Persistance/DapperDbContext.cs b/Kerem.CodingTracker/Kerem.CodingTracker/Infrastructure/Persistance/DapperDbContext.cs new file mode 100644 index 000000000..5ea36f6ba --- /dev/null +++ b/Kerem.CodingTracker/Kerem.CodingTracker/Infrastructure/Persistance/DapperDbContext.cs @@ -0,0 +1,34 @@ +using System.Data; +using Microsoft.Data.SqlClient; + +namespace Kerem.CodingTracker ; + + public class DapperDbContext : IDisposable + { + private readonly string _connectionString; + private IDbConnection _connection; + + public DapperDbContext(string connectionString) + { + _connectionString = connectionString; + } + + public IDbConnection GetConnection() + { + if (_connection == null || _connection.State != ConnectionState.Open) + { + _connection = new SqlConnection(_connectionString); + _connection.Open(); + } + return _connection; + } + + public void Dispose() + { + if (_connection != null && _connection.State == ConnectionState.Open) + { + _connection.Close(); + _connection.Dispose(); + } + } + } \ No newline at end of file diff --git a/Kerem.CodingTracker/Kerem.CodingTracker/Infrastructure/Repositories/CodingSessionRepository.cs b/Kerem.CodingTracker/Kerem.CodingTracker/Infrastructure/Repositories/CodingSessionRepository.cs new file mode 100644 index 000000000..b963a0841 --- /dev/null +++ b/Kerem.CodingTracker/Kerem.CodingTracker/Infrastructure/Repositories/CodingSessionRepository.cs @@ -0,0 +1,30 @@ +using Dapper; +using Kerem.CodingTracker.Domain.Interfaces; + +namespace Kerem.CodingTracker.Infrastructure.Repositories ; + + public class CodingSessionRepository : ICodingSessionRepository + { + private readonly DapperDbContext _dapperDbContext; + + public CodingSessionRepository(DapperDbContext context) + { + _dapperDbContext = context; + } + public List FindAll() + { + var connection = _dapperDbContext.GetConnection(); + var sql = "SELECT * FROM CodingSession"; + var codingSessions = connection.Query(sql).ToList(); + return codingSessions; + } + + public void Create(CodingSession codingSession) + { + var connection = _dapperDbContext.GetConnection(); + var sql = "INSERT INTO CodingSession (startTime, endTime, duration) VALUES (@StartTime, @EndTime, @Duration)"; + connection.Execute(sql, codingSession); + } + + + } \ No newline at end of file diff --git a/Kerem.CodingTracker/Kerem.CodingTracker/Kerem.CodingTracker.csproj b/Kerem.CodingTracker/Kerem.CodingTracker/Kerem.CodingTracker.csproj new file mode 100644 index 000000000..5d34f8d8b --- /dev/null +++ b/Kerem.CodingTracker/Kerem.CodingTracker/Kerem.CodingTracker.csproj @@ -0,0 +1,16 @@ + + + + Exe + net8.0 + enable + enable + + + + + + + + + diff --git a/Kerem.CodingTracker/Kerem.CodingTracker/Program.cs b/Kerem.CodingTracker/Kerem.CodingTracker/Program.cs new file mode 100644 index 000000000..1ee3be331 --- /dev/null +++ b/Kerem.CodingTracker/Kerem.CodingTracker/Program.cs @@ -0,0 +1,12 @@ +using Kerem.CodingTracker; +using Kerem.CodingTracker.Domain.Interfaces; +using Kerem.CodingTracker.Features.CreateCodingSession; +using Kerem.CodingTracker.Infrastructure.Repositories; +using Kerem.CodingTracker.UI; + +ConsoleMenu startProgram = new ConsoleMenu(); +var connectionString = @"Data Source=localhost\SQLEXPRESS;Initial Catalog=CodingTracker;Integrated Security=true;TrustServerCertificate=true;"; +DapperDbContext dapperDbContext = new DapperDbContext(connectionString); +ICodingSessionRepository codingSessionRepository = new CodingSessionRepository(dapperDbContext); +CreateCodingSession createCodingSession = new CreateCodingSession(codingSessionRepository); +startProgram.Menu(createCodingSession); \ No newline at end of file diff --git a/Kerem.CodingTracker/Kerem.CodingTracker/UI/ConsoleMenu.cs b/Kerem.CodingTracker/Kerem.CodingTracker/UI/ConsoleMenu.cs new file mode 100644 index 000000000..198b44349 --- /dev/null +++ b/Kerem.CodingTracker/Kerem.CodingTracker/UI/ConsoleMenu.cs @@ -0,0 +1,48 @@ +using Kerem.CodingTracker.Features.CreateCodingSession; +using Kerem.CodingTracker.Infrastructure.Repositories; + +namespace Kerem.CodingTracker.UI ; + + public class ConsoleMenu + { + public void Menu(CreateCodingSession createCodingSession) + { + bool exit = false; + Console.WriteLine("Welcome to the Coding Tracker!"); + Console.WriteLine(); + while (!exit) + { + Console.WriteLine(); + Console.WriteLine("Please select an option:"); + Console.WriteLine("1. Create a new coding session"); + Console.WriteLine("2. View all coding sessions"); + Console.WriteLine("3. Delete a coding session"); + Console.WriteLine("4. Update a coding session"); + Console.WriteLine("5. Exit"); + int choice = int.TryParse(Console.ReadLine() ?? string.Empty, out choice) ? choice : 0; + Console.WriteLine(); + switch (choice) + { + case 1: + createCodingSession.Create(); + break; + case 2: + break; + case 3: + break; + case 4: + break; + case 5: + exit = true; + Console.WriteLine(); + Console.WriteLine("Goodbye!"); + break; + default: + Console.WriteLine(); + Console.WriteLine("Invalid option."); + break; + } + } + } + } + \ No newline at end of file diff --git a/Kerem.CodingTracker/global.json b/Kerem.CodingTracker/global.json new file mode 100644 index 000000000..2ddda36c2 --- /dev/null +++ b/Kerem.CodingTracker/global.json @@ -0,0 +1,7 @@ +{ + "sdk": { + "version": "8.0.0", + "rollForward": "latestMinor", + "allowPrerelease": false + } +} \ No newline at end of file From c5b18a9a8a152d44e79e69323e6050fbd89ffabb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Kerem=20Bj=C3=A4lven=C3=A4s=20Tazedal?= <98690898+Kerem1989@users.noreply.github.com> Date: Wed, 5 Aug 2026 08:43:58 +0200 Subject: [PATCH 2/6] Added the spectre console --- .../Domain/Entities/CodingSession.cs | 10 ++-- .../Interfaces/ICodingSessionRepository.cs | 4 +- .../CreateCodingSession.cs | 53 +++++++++++++++---- .../CreateCodingSessionValidator.cs | 2 +- .../Repositories/CodingSessionRepository.cs | 1 + .../Kerem.CodingTracker/UI/ConsoleMenu.cs | 1 - 6 files changed, 52 insertions(+), 19 deletions(-) diff --git a/Kerem.CodingTracker/Kerem.CodingTracker/Domain/Entities/CodingSession.cs b/Kerem.CodingTracker/Kerem.CodingTracker/Domain/Entities/CodingSession.cs index 9917cae5b..7ae93ee96 100644 --- a/Kerem.CodingTracker/Kerem.CodingTracker/Domain/Entities/CodingSession.cs +++ b/Kerem.CodingTracker/Kerem.CodingTracker/Domain/Entities/CodingSession.cs @@ -1,9 +1,9 @@ -namespace Kerem.CodingTracker ; +namespace Kerem.CodingTracker.Domain.Entities ; public class CodingSession { - public int id { get; set; } - public DateTime startTime { get; set; } - public DateTime endTime { get; set; } - public Decimal duration { get; set; } + public int Id { get; set; } + public DateTime StartTime { get; set; } + public DateTime EndTime { get; set; } + public Decimal Duration { get; set; } } \ No newline at end of file diff --git a/Kerem.CodingTracker/Kerem.CodingTracker/Domain/Interfaces/ICodingSessionRepository.cs b/Kerem.CodingTracker/Kerem.CodingTracker/Domain/Interfaces/ICodingSessionRepository.cs index 3952d6089..50d5b93d1 100644 --- a/Kerem.CodingTracker/Kerem.CodingTracker/Domain/Interfaces/ICodingSessionRepository.cs +++ b/Kerem.CodingTracker/Kerem.CodingTracker/Domain/Interfaces/ICodingSessionRepository.cs @@ -1,4 +1,6 @@ -namespace Kerem.CodingTracker.Domain.Interfaces ; +using Kerem.CodingTracker.Domain.Entities; + +namespace Kerem.CodingTracker.Domain.Interfaces ; public interface ICodingSessionRepository { diff --git a/Kerem.CodingTracker/Kerem.CodingTracker/Features/CreateCodingSession/CreateCodingSession.cs b/Kerem.CodingTracker/Kerem.CodingTracker/Features/CreateCodingSession/CreateCodingSession.cs index bd306898e..1037edee3 100644 --- a/Kerem.CodingTracker/Kerem.CodingTracker/Features/CreateCodingSession/CreateCodingSession.cs +++ b/Kerem.CodingTracker/Kerem.CodingTracker/Features/CreateCodingSession/CreateCodingSession.cs @@ -1,6 +1,6 @@ -using System.Data; +using Kerem.CodingTracker.Domain.Entities; using Kerem.CodingTracker.Domain.Interfaces; -using Kerem.CodingTracker.Infrastructure.Repositories; +using Spectre.Console; namespace Kerem.CodingTracker.Features.CreateCodingSession ; @@ -16,26 +16,57 @@ public CreateCodingSession(ICodingSessionRepository codingSessionRepository) public void Create() { CodingSession codingSession = new CodingSession(); - Console.WriteLine("Please enter the start date"); + AnsiConsole.MarkupLine("[bold steelblue]Please enter the start date in the format of yyyy-mm-dd hh:mm[/]"); - var startDate = Console.ReadLine(); - var correctFomat = CreateCodingSessionValidator.ValidateDateFormat(startDate); + + var startDate = Console.ReadLine() ?? " "; + + var emptyStartDate = string.IsNullOrEmpty(startDate); + if (emptyStartDate) + { + AnsiConsole.MarkupLine("[red]Date cannot be empty[/]"); + + return; + } + + + var correctFormat = CreateCodingSessionValidator.ValidateDateFormat(startDate); - if (!correctFomat) + if (!correctFormat) { Console.WriteLine("Format is invalid"); return; } DateTime startTime = DateTime.Parse(startDate); - codingSession.startTime = startTime; + codingSession.StartTime = startTime; Console.WriteLine("Please enter the end date"); - var endDate = Console.ReadLine(); + var endDate = Console.ReadLine() ?? " "; + var emptyEndDate = string.IsNullOrEmpty(endDate); + + if (emptyEndDate) + { + Console.WriteLine("Date cannot be empty"); + return; + } + + correctFormat = CreateCodingSessionValidator.ValidateDateFormat(endDate); + + if (!correctFormat) + { + Console.WriteLine("Format is invalid"); + return; + } + DateTime endTime = DateTime.Parse(endDate); - codingSession.endTime = endTime; - decimal durationInSeconds = Convert.ToDecimal(DateTime.Now.Subtract(startTime).TotalSeconds); - codingSession.duration = 0; + codingSession.EndTime = endTime; + + + var difference = endTime - startTime; + var minutes = (int)difference.TotalMinutes; + codingSession.Duration = minutes; + _codingSessionRepository.Create(codingSession); } } \ No newline at end of file diff --git a/Kerem.CodingTracker/Kerem.CodingTracker/Features/CreateCodingSession/CreateCodingSessionValidator.cs b/Kerem.CodingTracker/Kerem.CodingTracker/Features/CreateCodingSession/CreateCodingSessionValidator.cs index c7e87372f..8935ae5fa 100644 --- a/Kerem.CodingTracker/Kerem.CodingTracker/Features/CreateCodingSession/CreateCodingSessionValidator.cs +++ b/Kerem.CodingTracker/Kerem.CodingTracker/Features/CreateCodingSession/CreateCodingSessionValidator.cs @@ -6,7 +6,7 @@ public static class CreateCodingSessionValidator { public static bool ValidateDateFormat(String date) { - string pattern = @"^\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2}$"; + string pattern = @"^\d{4}-\d{2}-\d{2} \d{2}:\d{2}$"; if (Regex.IsMatch(date, pattern)) { return true; diff --git a/Kerem.CodingTracker/Kerem.CodingTracker/Infrastructure/Repositories/CodingSessionRepository.cs b/Kerem.CodingTracker/Kerem.CodingTracker/Infrastructure/Repositories/CodingSessionRepository.cs index b963a0841..8d5f413d2 100644 --- a/Kerem.CodingTracker/Kerem.CodingTracker/Infrastructure/Repositories/CodingSessionRepository.cs +++ b/Kerem.CodingTracker/Kerem.CodingTracker/Infrastructure/Repositories/CodingSessionRepository.cs @@ -1,4 +1,5 @@ using Dapper; +using Kerem.CodingTracker.Domain.Entities; using Kerem.CodingTracker.Domain.Interfaces; namespace Kerem.CodingTracker.Infrastructure.Repositories ; diff --git a/Kerem.CodingTracker/Kerem.CodingTracker/UI/ConsoleMenu.cs b/Kerem.CodingTracker/Kerem.CodingTracker/UI/ConsoleMenu.cs index 198b44349..f23d3c691 100644 --- a/Kerem.CodingTracker/Kerem.CodingTracker/UI/ConsoleMenu.cs +++ b/Kerem.CodingTracker/Kerem.CodingTracker/UI/ConsoleMenu.cs @@ -1,5 +1,4 @@ using Kerem.CodingTracker.Features.CreateCodingSession; -using Kerem.CodingTracker.Infrastructure.Repositories; namespace Kerem.CodingTracker.UI ; From 3f8d4ac0c2877e232055363aa3f1fd295d4a8d21 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Kerem=20Bj=C3=A4lven=C3=A4s=20Tazedal?= <98690898+Kerem1989@users.noreply.github.com> Date: Sun, 23 Aug 2026 10:16:55 +0200 Subject: [PATCH 3/6] Added crud - Create and Edit codingsessions --- .../DependencyInjection.cs | 24 ++++++ .../Interfaces/ICodingSessionRepository.cs | 5 +- .../CountCodingSession/CountCodingSession.cs | 20 +++++ .../CreateCodingSession.cs | 33 ++++++-- .../EditCodingSession/EditCodingSession.cs | 75 ++++++++++++++++++ .../FindAllCodingSession.cs | 41 ++++++++++ .../Repositories/CodingSessionRepository.cs | 23 ++++++ .../Utils/Validator.cs} | 11 ++- .../Kerem.CodingTracker.csproj | 7 +- .../Kerem.CodingTracker/Program.cs | 22 +++--- .../Kerem.CodingTracker/UI/ConsoleMenu.cs | 76 +++++++++++-------- .../Kerem.CodingTracker/appsettings.json | 12 +++ 12 files changed, 299 insertions(+), 50 deletions(-) create mode 100644 Kerem.CodingTracker/Kerem.CodingTracker/DependencyInjection.cs create mode 100644 Kerem.CodingTracker/Kerem.CodingTracker/Features/CountCodingSession/CountCodingSession.cs create mode 100644 Kerem.CodingTracker/Kerem.CodingTracker/Features/EditCodingSession/EditCodingSession.cs create mode 100644 Kerem.CodingTracker/Kerem.CodingTracker/Features/FindAllCodingSession/FindAllCodingSession.cs rename Kerem.CodingTracker/Kerem.CodingTracker/{Features/CreateCodingSession/CreateCodingSessionValidator.cs => Infrastructure/Utils/Validator.cs} (64%) create mode 100644 Kerem.CodingTracker/Kerem.CodingTracker/appsettings.json diff --git a/Kerem.CodingTracker/Kerem.CodingTracker/DependencyInjection.cs b/Kerem.CodingTracker/Kerem.CodingTracker/DependencyInjection.cs new file mode 100644 index 000000000..55560cfb8 --- /dev/null +++ b/Kerem.CodingTracker/Kerem.CodingTracker/DependencyInjection.cs @@ -0,0 +1,24 @@ +using Kerem.CodingTracker.Domain.Interfaces; +using Kerem.CodingTracker.Features.CreateCodingSession; +using Kerem.CodingTracker.Features.EditCodingSession; +using Kerem.CodingTracker.Features.FindAllCodingSession; +using Kerem.CodingTracker.Infrastructure.Repositories; +using Kerem.CodingTracker.UI; +using Microsoft.Extensions.DependencyInjection; + +namespace Kerem.CodingTracker ; + + public static class DependencyInjection + { + public static IServiceCollection AddApplication(this IServiceCollection services, string connectionString) + { + services.AddSingleton(new DapperDbContext(connectionString)); + services.AddSingleton(); + services.AddSingleton(); + services.AddSingleton(); + services.AddSingleton(); + services.AddSingleton(); + services.AddSingleton(); + return services; + } + } \ No newline at end of file diff --git a/Kerem.CodingTracker/Kerem.CodingTracker/Domain/Interfaces/ICodingSessionRepository.cs b/Kerem.CodingTracker/Kerem.CodingTracker/Domain/Interfaces/ICodingSessionRepository.cs index 50d5b93d1..b28a7de46 100644 --- a/Kerem.CodingTracker/Kerem.CodingTracker/Domain/Interfaces/ICodingSessionRepository.cs +++ b/Kerem.CodingTracker/Kerem.CodingTracker/Domain/Interfaces/ICodingSessionRepository.cs @@ -4,6 +4,9 @@ namespace Kerem.CodingTracker.Domain.Interfaces ; public interface ICodingSessionRepository { - List FindAll(); + List ? FindAll(); void Create(CodingSession codingSession); + int CountCodingSessions(); + CodingSession? FindById(int id); + void Save(CodingSession codingSession); } \ No newline at end of file diff --git a/Kerem.CodingTracker/Kerem.CodingTracker/Features/CountCodingSession/CountCodingSession.cs b/Kerem.CodingTracker/Kerem.CodingTracker/Features/CountCodingSession/CountCodingSession.cs new file mode 100644 index 000000000..c2112ae84 --- /dev/null +++ b/Kerem.CodingTracker/Kerem.CodingTracker/Features/CountCodingSession/CountCodingSession.cs @@ -0,0 +1,20 @@ +using Kerem.CodingTracker.Domain.Interfaces; +using Spectre.Console; + +namespace Kerem.CodingTracker.Features.CreateCodingSession ; + + public class CountCodingSession + { + private readonly ICodingSessionRepository _codingSessionRepository; + + public CountCodingSession(ICodingSessionRepository codingSessionRepository) + { + _codingSessionRepository = codingSessionRepository; + } + + public void CountCodingSessions() + { + var amount = _codingSessionRepository.CountCodingSessions(); + AnsiConsole.MarkupLine($"[red]There are {amount} registered coding sessions in the database.[/]"); + } + } \ No newline at end of file diff --git a/Kerem.CodingTracker/Kerem.CodingTracker/Features/CreateCodingSession/CreateCodingSession.cs b/Kerem.CodingTracker/Kerem.CodingTracker/Features/CreateCodingSession/CreateCodingSession.cs index 1037edee3..ee5f4daf5 100644 --- a/Kerem.CodingTracker/Kerem.CodingTracker/Features/CreateCodingSession/CreateCodingSession.cs +++ b/Kerem.CodingTracker/Kerem.CodingTracker/Features/CreateCodingSession/CreateCodingSession.cs @@ -17,9 +17,17 @@ public void Create() { CodingSession codingSession = new CodingSession(); AnsiConsole.MarkupLine("[bold steelblue]Please enter the start date in the format of yyyy-mm-dd hh:mm[/]"); - + AnsiConsole.MarkupLine("[Orange1]Enter abort to exit back to the main menu[/]"); var startDate = Console.ReadLine() ?? " "; + bool shouldAbort = Validator.Abort(startDate); + + if (shouldAbort) + { + AnsiConsole.MarkupLine("[Orange1]Aborted[/]"); + return; + } + var emptyStartDate = string.IsNullOrEmpty(startDate); if (emptyStartDate) @@ -30,32 +38,42 @@ public void Create() } - var correctFormat = CreateCodingSessionValidator.ValidateDateFormat(startDate); + var correctFormat = Validator.ValidateDateFormat(startDate); if (!correctFormat) { - Console.WriteLine("Format is invalid"); + AnsiConsole.MarkupLine("[red]Format is invalid[/]"); return; } DateTime startTime = DateTime.Parse(startDate); codingSession.StartTime = startTime; - Console.WriteLine("Please enter the end date"); + AnsiConsole.MarkupLine("[bold steelblue]Please enter the end date in the format of yyyy-mm-dd hh:mm[/]"); + var endDate = Console.ReadLine() ?? " "; + + shouldAbort = Validator.Abort(startDate); + + if (shouldAbort) + { + AnsiConsole.MarkupLine("[Orange1]Aborted[/]"); + return; + } + var emptyEndDate = string.IsNullOrEmpty(endDate); if (emptyEndDate) { - Console.WriteLine("Date cannot be empty"); + AnsiConsole.MarkupLine("[red]Date cannot be empty[/]"); return; } - correctFormat = CreateCodingSessionValidator.ValidateDateFormat(endDate); + correctFormat = Validator.ValidateDateFormat(endDate); if (!correctFormat) { - Console.WriteLine("Format is invalid"); + AnsiConsole.MarkupLine("[red]Format is invalid[/]"); return; } @@ -68,5 +86,6 @@ public void Create() codingSession.Duration = minutes; _codingSessionRepository.Create(codingSession); + AnsiConsole.MarkupLine("[green]Coding session created[/]"); } } \ No newline at end of file diff --git a/Kerem.CodingTracker/Kerem.CodingTracker/Features/EditCodingSession/EditCodingSession.cs b/Kerem.CodingTracker/Kerem.CodingTracker/Features/EditCodingSession/EditCodingSession.cs new file mode 100644 index 000000000..35c060a44 --- /dev/null +++ b/Kerem.CodingTracker/Kerem.CodingTracker/Features/EditCodingSession/EditCodingSession.cs @@ -0,0 +1,75 @@ +using Kerem.CodingTracker.Domain.Entities; +using Kerem.CodingTracker.Domain.Interfaces; +using Kerem.CodingTracker.Features.CreateCodingSession; +using Spectre.Console; + +namespace Kerem.CodingTracker.Features.EditCodingSession ; + + public class EditCodingSession + { + private readonly ICodingSessionRepository _codingSessionRepository; + + public EditCodingSession(ICodingSessionRepository codingSessionRepository) + { + _codingSessionRepository = codingSessionRepository; + } + + public void CodingSessionEdit() + { + AnsiConsole.MarkupLine("[blue]Please enter the id of the coding session you want to edit[/]"); + var id = Convert.ToInt32(Console.ReadLine()); + var codingSession = _codingSessionRepository.FindById(id); + if (codingSession == null) + { + AnsiConsole.MarkupLine("[red]The coding session you want to edit does not exist[/]"); + return; + } + + bool runProgram = true; + while (runProgram) + { + AnsiConsole.MarkupLine("[blue]Please select 1 to edit startime, 2 to edit end date or 3 to exit[/]"); + int selectedProperty = Convert.ToInt32(Console.ReadLine()); + switch (selectedProperty) + { + case 1: + AnsiConsole.MarkupLine("[bold steelblue]Please enter the start date in the format of yyyy-mm-dd hh:mm[/]"); + var startDate = Console.ReadLine() ?? " "; + var emptyStartDate = string.IsNullOrEmpty(startDate); + if (emptyStartDate) + { + AnsiConsole.MarkupLine("[red]Date cannot be empty[/]"); + + return; + } + + + var correctFormat = Validator.ValidateDateFormat(startDate); + + if (!correctFormat) + { + AnsiConsole.MarkupLine("[red]Format is invalid[/]"); + return; + } + DateTime startTime = DateTime.Parse(startDate); + + codingSession.StartTime = startTime; + break; + case 2: + AnsiConsole.MarkupLine("[blue]Enter a new end date[/]"); + var endDate = Console.ReadLine() ?? " "; + DateTime endTime = DateTime.Parse(endDate); + codingSession.EndTime = endTime; + break; + case 3: + runProgram = false; + break; + } + } + + var difference = codingSession.EndTime - codingSession.StartTime; + var minutes = (int)difference.TotalMinutes; + codingSession.Duration = minutes; + _codingSessionRepository.Save(codingSession); + } + } \ No newline at end of file diff --git a/Kerem.CodingTracker/Kerem.CodingTracker/Features/FindAllCodingSession/FindAllCodingSession.cs b/Kerem.CodingTracker/Kerem.CodingTracker/Features/FindAllCodingSession/FindAllCodingSession.cs new file mode 100644 index 000000000..41264f608 --- /dev/null +++ b/Kerem.CodingTracker/Kerem.CodingTracker/Features/FindAllCodingSession/FindAllCodingSession.cs @@ -0,0 +1,41 @@ +using Kerem.CodingTracker.Domain.Interfaces; +using Spectre.Console; + +namespace Kerem.CodingTracker.Features.FindAllCodingSession ; + + public class FindAllCodingSession + { + private readonly ICodingSessionRepository _codingSessionRepository; + + public FindAllCodingSession(ICodingSessionRepository codingSessionRepository) + { + _codingSessionRepository = codingSessionRepository; + } + + public void FindAll() + { + var amount = _codingSessionRepository.FindAll(); + + if (amount == null) + { + AnsiConsole.MarkupLine("[red]There are no registered coding sessions in the database.[/]"); + return; + } + + var table = new Table() + .RoundedBorder() + .BorderColor(Color.Green); + + table.AddColumn(("Id")); + table.AddColumn(("Start Time")); + table.AddColumn(("End Time")); + table.AddColumn(("Duration")); + + foreach (var codingSession in amount) + { + table.AddRow($"{codingSession.Id}", $"{codingSession.StartTime}", $"{codingSession.EndTime}", $"{codingSession.Duration}"); + + } + AnsiConsole.Write(table); + } + } \ No newline at end of file diff --git a/Kerem.CodingTracker/Kerem.CodingTracker/Infrastructure/Repositories/CodingSessionRepository.cs b/Kerem.CodingTracker/Kerem.CodingTracker/Infrastructure/Repositories/CodingSessionRepository.cs index 8d5f413d2..107227cb9 100644 --- a/Kerem.CodingTracker/Kerem.CodingTracker/Infrastructure/Repositories/CodingSessionRepository.cs +++ b/Kerem.CodingTracker/Kerem.CodingTracker/Infrastructure/Repositories/CodingSessionRepository.cs @@ -1,6 +1,7 @@ using Dapper; using Kerem.CodingTracker.Domain.Entities; using Kerem.CodingTracker.Domain.Interfaces; +using Spectre.Console; namespace Kerem.CodingTracker.Infrastructure.Repositories ; @@ -26,6 +27,28 @@ public void Create(CodingSession codingSession) var sql = "INSERT INTO CodingSession (startTime, endTime, duration) VALUES (@StartTime, @EndTime, @Duration)"; connection.Execute(sql, codingSession); } + + public int CountCodingSessions() + { + var connection = _dapperDbContext.GetConnection(); + var sql = "SELECT COUNT(*) FROM CodingSession"; + return connection.ExecuteScalar(sql); + } + + public CodingSession FindById(int id) + { + var connection = _dapperDbContext.GetConnection(); + var sql = "SELECT * FROM CodingSession WHERE Id = @Id"; + var codingSession = connection.Query(sql, new { Id = id }).FirstOrDefault(); + return codingSession; + } + + public void Save(CodingSession codingSession) + { + var connection = _dapperDbContext.GetConnection(); + var sql = $"UPDATE CodingSession SET startTime = @StartTime, duration = @Duration WHERE Id = @Id"; + connection.ExecuteScalar(sql, new { codingSession.StartTime, codingSession.Duration, codingSession.Id }); + } } \ No newline at end of file diff --git a/Kerem.CodingTracker/Kerem.CodingTracker/Features/CreateCodingSession/CreateCodingSessionValidator.cs b/Kerem.CodingTracker/Kerem.CodingTracker/Infrastructure/Utils/Validator.cs similarity index 64% rename from Kerem.CodingTracker/Kerem.CodingTracker/Features/CreateCodingSession/CreateCodingSessionValidator.cs rename to Kerem.CodingTracker/Kerem.CodingTracker/Infrastructure/Utils/Validator.cs index 8935ae5fa..c08875c99 100644 --- a/Kerem.CodingTracker/Kerem.CodingTracker/Features/CreateCodingSession/CreateCodingSessionValidator.cs +++ b/Kerem.CodingTracker/Kerem.CodingTracker/Infrastructure/Utils/Validator.cs @@ -2,7 +2,7 @@ namespace Kerem.CodingTracker.Features.CreateCodingSession ; - public static class CreateCodingSessionValidator + public static class Validator { public static bool ValidateDateFormat(String date) { @@ -13,5 +13,14 @@ public static bool ValidateDateFormat(String date) } return false; } + + public static bool Abort(string choice) + { + if (choice == "abort") + { + return true; + } + return false; + } } \ No newline at end of file diff --git a/Kerem.CodingTracker/Kerem.CodingTracker/Kerem.CodingTracker.csproj b/Kerem.CodingTracker/Kerem.CodingTracker/Kerem.CodingTracker.csproj index 5d34f8d8b..2e54ea823 100644 --- a/Kerem.CodingTracker/Kerem.CodingTracker/Kerem.CodingTracker.csproj +++ b/Kerem.CodingTracker/Kerem.CodingTracker/Kerem.CodingTracker.csproj @@ -10,7 +10,12 @@ - + + + + + PreserveNewest + diff --git a/Kerem.CodingTracker/Kerem.CodingTracker/Program.cs b/Kerem.CodingTracker/Kerem.CodingTracker/Program.cs index 1ee3be331..0f38f353b 100644 --- a/Kerem.CodingTracker/Kerem.CodingTracker/Program.cs +++ b/Kerem.CodingTracker/Kerem.CodingTracker/Program.cs @@ -1,12 +1,16 @@ using Kerem.CodingTracker; -using Kerem.CodingTracker.Domain.Interfaces; -using Kerem.CodingTracker.Features.CreateCodingSession; -using Kerem.CodingTracker.Infrastructure.Repositories; using Kerem.CodingTracker.UI; +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.DependencyInjection; -ConsoleMenu startProgram = new ConsoleMenu(); -var connectionString = @"Data Source=localhost\SQLEXPRESS;Initial Catalog=CodingTracker;Integrated Security=true;TrustServerCertificate=true;"; -DapperDbContext dapperDbContext = new DapperDbContext(connectionString); -ICodingSessionRepository codingSessionRepository = new CodingSessionRepository(dapperDbContext); -CreateCodingSession createCodingSession = new CreateCodingSession(codingSessionRepository); -startProgram.Menu(createCodingSession); \ No newline at end of file +var builder = new ConfigurationBuilder() + .SetBasePath(Directory.GetCurrentDirectory()) + .AddJsonFile("appsettings.json", optional: false); +IConfiguration config = builder.Build(); +var connectionString = config.GetConnectionString("DefaultConnection"); + +var serviceProvider = new ServiceCollection() + .AddApplication(connectionString) + .BuildServiceProvider(); + +serviceProvider.GetRequiredService().Menu(); diff --git a/Kerem.CodingTracker/Kerem.CodingTracker/UI/ConsoleMenu.cs b/Kerem.CodingTracker/Kerem.CodingTracker/UI/ConsoleMenu.cs index f23d3c691..5d78d138e 100644 --- a/Kerem.CodingTracker/Kerem.CodingTracker/UI/ConsoleMenu.cs +++ b/Kerem.CodingTracker/Kerem.CodingTracker/UI/ConsoleMenu.cs @@ -1,47 +1,61 @@ -using Kerem.CodingTracker.Features.CreateCodingSession; +using Kerem.CodingTracker.Features; +using Kerem.CodingTracker.Features.CreateCodingSession; +using Kerem.CodingTracker.Features.EditCodingSession; +using Kerem.CodingTracker.Features.FindAllCodingSession; +using Spectre.Console; namespace Kerem.CodingTracker.UI ; public class ConsoleMenu { - public void Menu(CreateCodingSession createCodingSession) + private readonly CreateCodingSession _createCodingSession; + private readonly CountCodingSession _countCodingSession; + private readonly FindAllCodingSession _findAllCodingSession; + private readonly EditCodingSession _editCodingSession; + + public ConsoleMenu(CreateCodingSession createCodingSession, CountCodingSession countCodingSession, FindAllCodingSession findAllCodingSession, EditCodingSession editCodingSession ) + { + _createCodingSession = createCodingSession; + _countCodingSession = countCodingSession; + _findAllCodingSession = findAllCodingSession; + _editCodingSession = editCodingSession; + } + + public void Menu() { - bool exit = false; - Console.WriteLine("Welcome to the Coding Tracker!"); - Console.WriteLine(); - while (!exit) + AnsiConsole.Write(new FigletText("Coding Tracker").Color(Color.SteelBlue)); + while (true) { - Console.WriteLine(); - Console.WriteLine("Please select an option:"); - Console.WriteLine("1. Create a new coding session"); - Console.WriteLine("2. View all coding sessions"); - Console.WriteLine("3. Delete a coding session"); - Console.WriteLine("4. Update a coding session"); - Console.WriteLine("5. Exit"); - int choice = int.TryParse(Console.ReadLine() ?? string.Empty, out choice) ? choice : 0; - Console.WriteLine(); - switch (choice) + string choice = AnsiConsole.Prompt( + new SelectionPrompt() + .Title("Please select an [bold steelblue]option[/]:") + .AddChoices( + "1. View all coding sessions", + "2. Create a coding session", + "3. Edit a coding session", + "4. Delete a coding session", + "5. Exit")); + + switch (choice[0]) { - case 1: - createCodingSession.Create(); - break; - case 2: - break; - case 3: + case '1': + _countCodingSession.CountCodingSessions(); + _findAllCodingSession.FindAll(); break; - case 4: + case '2': + _createCodingSession.Create(); break; - case 5: - exit = true; - Console.WriteLine(); - Console.WriteLine("Goodbye!"); + case '3': + _findAllCodingSession.FindAll(); + _editCodingSession.CodingSessionEdit(); break; - default: - Console.WriteLine(); - Console.WriteLine("Invalid option."); + case '4': break; + case '5': + AnsiConsole.MarkupLine("[bold green]Goodbye![/]"); + return; } } } } - \ No newline at end of file + diff --git a/Kerem.CodingTracker/Kerem.CodingTracker/appsettings.json b/Kerem.CodingTracker/Kerem.CodingTracker/appsettings.json new file mode 100644 index 000000000..3097bcae1 --- /dev/null +++ b/Kerem.CodingTracker/Kerem.CodingTracker/appsettings.json @@ -0,0 +1,12 @@ +{ + "Logging": { + "LogLevel": { + "Default": "Information", + "Microsoft": "Warning", + "System": "Error" + } + }, + "ConnectionStrings": { + "DefaultConnection": "Data Source=localhost\\SQLEXPRESS;Initial Catalog=CodingTracker;Integrated Security=true;TrustServerCertificate=true;" + } +} \ No newline at end of file From 83de0b02f9210cdcd3e507194bad0d6c63e8f71c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Kerem=20Bj=C3=A4lven=C3=A4s=20Tazedal?= <98690898+Kerem1989@users.noreply.github.com> Date: Mon, 24 Aug 2026 13:06:56 +0200 Subject: [PATCH 4/6] Added a test and readme --- .../Kerem.CodingTracker.Tests.csproj | 27 ++++ .../ValidatorTests.cs | 79 +++++++++++ Kerem.CodingTracker/Kerem.CodingTracker.sln | 7 + .../DependencyInjection.cs | 2 + .../Domain/Entities/CodingSession.cs | 2 +- .../Interfaces/ICodingSessionRepository.cs | 2 + .../CreateCodingSession.cs | 16 +-- .../DeleteCodingSession.cs | 33 +++++ .../EditCodingSession/EditCodingSession.cs | 29 +++- .../Repositories/CodingSessionRepository.cs | 11 +- .../Infrastructure/Utils/Validator.cs | 10 ++ .../Kerem.CodingTracker/UI/ConsoleMenu.cs | 7 +- Kerem.CodingTracker/NuGet.config | 7 + Kerem.CodingTracker/README.md | 132 ++++++++++++++++++ 14 files changed, 347 insertions(+), 17 deletions(-) create mode 100644 Kerem.CodingTracker/Kerem.CodingTracker.Tests/Kerem.CodingTracker.Tests.csproj create mode 100644 Kerem.CodingTracker/Kerem.CodingTracker.Tests/ValidatorTests.cs create mode 100644 Kerem.CodingTracker/Kerem.CodingTracker/Features/DeleteCodingSession/DeleteCodingSession.cs create mode 100644 Kerem.CodingTracker/NuGet.config create mode 100644 Kerem.CodingTracker/README.md diff --git a/Kerem.CodingTracker/Kerem.CodingTracker.Tests/Kerem.CodingTracker.Tests.csproj b/Kerem.CodingTracker/Kerem.CodingTracker.Tests/Kerem.CodingTracker.Tests.csproj new file mode 100644 index 000000000..466c685eb --- /dev/null +++ b/Kerem.CodingTracker/Kerem.CodingTracker.Tests/Kerem.CodingTracker.Tests.csproj @@ -0,0 +1,27 @@ + + + + net8.0 + enable + enable + + false + true + + + + + + + + + + + + + + + + + + diff --git a/Kerem.CodingTracker/Kerem.CodingTracker.Tests/ValidatorTests.cs b/Kerem.CodingTracker/Kerem.CodingTracker.Tests/ValidatorTests.cs new file mode 100644 index 000000000..72eda44d4 --- /dev/null +++ b/Kerem.CodingTracker/Kerem.CodingTracker.Tests/ValidatorTests.cs @@ -0,0 +1,79 @@ +using Kerem.CodingTracker.Features.CreateCodingSession; + +namespace Kerem.CodingTracker.Tests; + +public class ValidatorTests +{ + [Theory] + [InlineData("2026-08-24 14:30", true)] + [InlineData("2026-01-01 00:00", true)] + [InlineData("2026-8-24 14:30", false)] + [InlineData("2026-08-24 14:3", false)] + [InlineData("24-08-2026 14:30", false)] + [InlineData("2026-08-24", false)] + [InlineData("2026-08-24T14:30", false)] + [InlineData("", false)] + [InlineData("not a date", false)] + public void ValidateDateFormat_ReturnsExpectedResult(string input, bool expected) + { + var result = Validator.ValidateDateFormat(input); + + Assert.Equal(expected, result); + } + + [Theory] + [InlineData("abort", true)] + [InlineData("Abort", false)] + [InlineData("ABORT", false)] + [InlineData("", false)] + [InlineData("2026-08-24 14:30", false)] + public void Abort_ReturnsExpectedResult(string input, bool expected) + { + var result = Validator.Abort(input); + + Assert.Equal(expected, result); + } + + [Fact] + public void ValidateStartAndEndDate_ReturnsTrue_WhenStartIsBeforeEnd() + { + var start = new DateTime(2026, 8, 24, 9, 0, 0); + var end = new DateTime(2026, 8, 24, 17, 0, 0); + + var result = Validator.ValidateStartAndEndDate(start, end); + + Assert.True(result); + } + + [Fact] + public void ValidateStartAndEndDate_ReturnsTrue_WhenStartEqualsEnd() + { + var same = new DateTime(2026, 8, 24, 9, 0, 0); + + var result = Validator.ValidateStartAndEndDate(same, same); + + Assert.True(result); + } + + [Fact] + public void ValidateStartAndEndDate_ReturnsFalse_WhenStartIsAfterEnd() + { + var start = new DateTime(2026, 8, 24, 17, 0, 0); + var end = new DateTime(2026, 8, 24, 9, 0, 0); + + var result = Validator.ValidateStartAndEndDate(start, end); + + Assert.False(result); + } + + [Fact] + public void ValidateStartAndEndDate_ReturnsFalse_WhenEndIsOnEarlierDay() + { + var start = new DateTime(2026, 8, 24, 9, 0, 0); + var end = new DateTime(2026, 8, 23, 9, 0, 0); + + var result = Validator.ValidateStartAndEndDate(start, end); + + Assert.False(result); + } +} diff --git a/Kerem.CodingTracker/Kerem.CodingTracker.sln b/Kerem.CodingTracker/Kerem.CodingTracker.sln index 3c8a2a0c5..1d723d49f 100644 --- a/Kerem.CodingTracker/Kerem.CodingTracker.sln +++ b/Kerem.CodingTracker/Kerem.CodingTracker.sln @@ -1,7 +1,10 @@  Microsoft Visual Studio Solution File, Format Version 12.00 +# Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Kerem.CodingTracker", "Kerem.CodingTracker\Kerem.CodingTracker.csproj", "{03B58CE3-2E2B-496D-BD1E-DD53FA74B4FE}" EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Kerem.CodingTracker.Tests", "Kerem.CodingTracker.Tests\Kerem.CodingTracker.Tests.csproj", "{E23F54F5-68A6-4329-97B5-5DCBB33F4594}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -12,5 +15,9 @@ Global {03B58CE3-2E2B-496D-BD1E-DD53FA74B4FE}.Debug|Any CPU.Build.0 = Debug|Any CPU {03B58CE3-2E2B-496D-BD1E-DD53FA74B4FE}.Release|Any CPU.ActiveCfg = Release|Any CPU {03B58CE3-2E2B-496D-BD1E-DD53FA74B4FE}.Release|Any CPU.Build.0 = Release|Any CPU + {E23F54F5-68A6-4329-97B5-5DCBB33F4594}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {E23F54F5-68A6-4329-97B5-5DCBB33F4594}.Debug|Any CPU.Build.0 = Debug|Any CPU + {E23F54F5-68A6-4329-97B5-5DCBB33F4594}.Release|Any CPU.ActiveCfg = Release|Any CPU + {E23F54F5-68A6-4329-97B5-5DCBB33F4594}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection EndGlobal diff --git a/Kerem.CodingTracker/Kerem.CodingTracker/DependencyInjection.cs b/Kerem.CodingTracker/Kerem.CodingTracker/DependencyInjection.cs index 55560cfb8..147f8a14d 100644 --- a/Kerem.CodingTracker/Kerem.CodingTracker/DependencyInjection.cs +++ b/Kerem.CodingTracker/Kerem.CodingTracker/DependencyInjection.cs @@ -1,5 +1,6 @@ using Kerem.CodingTracker.Domain.Interfaces; using Kerem.CodingTracker.Features.CreateCodingSession; +using Kerem.CodingTracker.Features.DeleteCodingSession; using Kerem.CodingTracker.Features.EditCodingSession; using Kerem.CodingTracker.Features.FindAllCodingSession; using Kerem.CodingTracker.Infrastructure.Repositories; @@ -19,6 +20,7 @@ public static IServiceCollection AddApplication(this IServiceCollection services services.AddSingleton(); services.AddSingleton(); services.AddSingleton(); + services.AddSingleton(); return services; } } \ No newline at end of file diff --git a/Kerem.CodingTracker/Kerem.CodingTracker/Domain/Entities/CodingSession.cs b/Kerem.CodingTracker/Kerem.CodingTracker/Domain/Entities/CodingSession.cs index 7ae93ee96..bda93248c 100644 --- a/Kerem.CodingTracker/Kerem.CodingTracker/Domain/Entities/CodingSession.cs +++ b/Kerem.CodingTracker/Kerem.CodingTracker/Domain/Entities/CodingSession.cs @@ -5,5 +5,5 @@ public class CodingSession public int Id { get; set; } public DateTime StartTime { get; set; } public DateTime EndTime { get; set; } - public Decimal Duration { get; set; } + public long Duration { get; set; } } \ No newline at end of file diff --git a/Kerem.CodingTracker/Kerem.CodingTracker/Domain/Interfaces/ICodingSessionRepository.cs b/Kerem.CodingTracker/Kerem.CodingTracker/Domain/Interfaces/ICodingSessionRepository.cs index b28a7de46..84d3b573b 100644 --- a/Kerem.CodingTracker/Kerem.CodingTracker/Domain/Interfaces/ICodingSessionRepository.cs +++ b/Kerem.CodingTracker/Kerem.CodingTracker/Domain/Interfaces/ICodingSessionRepository.cs @@ -9,4 +9,6 @@ public interface ICodingSessionRepository int CountCodingSessions(); CodingSession? FindById(int id); void Save(CodingSession codingSession); + + void Delete(int id); } \ No newline at end of file diff --git a/Kerem.CodingTracker/Kerem.CodingTracker/Features/CreateCodingSession/CreateCodingSession.cs b/Kerem.CodingTracker/Kerem.CodingTracker/Features/CreateCodingSession/CreateCodingSession.cs index ee5f4daf5..cc3093f5f 100644 --- a/Kerem.CodingTracker/Kerem.CodingTracker/Features/CreateCodingSession/CreateCodingSession.cs +++ b/Kerem.CodingTracker/Kerem.CodingTracker/Features/CreateCodingSession/CreateCodingSession.cs @@ -53,14 +53,6 @@ public void Create() var endDate = Console.ReadLine() ?? " "; - shouldAbort = Validator.Abort(startDate); - - if (shouldAbort) - { - AnsiConsole.MarkupLine("[Orange1]Aborted[/]"); - return; - } - var emptyEndDate = string.IsNullOrEmpty(endDate); if (emptyEndDate) @@ -80,6 +72,14 @@ public void Create() DateTime endTime = DateTime.Parse(endDate); codingSession.EndTime = endTime; + var correctDateTime = Validator.ValidateStartAndEndDate(codingSession.StartTime, codingSession.EndTime); + + if (!correctDateTime) + { + AnsiConsole.MarkupLine("[red]Start date cannot be later than end date[/]"); + return; + } + var difference = endTime - startTime; var minutes = (int)difference.TotalMinutes; diff --git a/Kerem.CodingTracker/Kerem.CodingTracker/Features/DeleteCodingSession/DeleteCodingSession.cs b/Kerem.CodingTracker/Kerem.CodingTracker/Features/DeleteCodingSession/DeleteCodingSession.cs new file mode 100644 index 000000000..af5024eef --- /dev/null +++ b/Kerem.CodingTracker/Kerem.CodingTracker/Features/DeleteCodingSession/DeleteCodingSession.cs @@ -0,0 +1,33 @@ +using Kerem.CodingTracker.Domain.Interfaces; +using Spectre.Console; + +namespace Kerem.CodingTracker.Features.DeleteCodingSession ; + + public class DeleteCodingSession + { + private readonly ICodingSessionRepository _codingSessionRepository; + + public DeleteCodingSession(ICodingSessionRepository codingSessionRepository) + { + _codingSessionRepository = codingSessionRepository; + } + public void DeleteCodingSessionById() + { + AnsiConsole.MarkupLine("[blue]Please enter the id of the coding session you want to delete[/]"); + int selectedSesssion = int.TryParse(Console.ReadLine(), out var selectedId) ? selectedId : 0; + if (selectedSesssion == 0) + { + AnsiConsole.MarkupLine("[red]The input its not a numerical value[/]"); + return; + } + var codingSession = _codingSessionRepository.FindById(selectedSesssion); + if (codingSession == null) + { + AnsiConsole.MarkupLine("[red]The coding session you want to delete does not exist[/]"); + return; + } + _codingSessionRepository.Delete(codingSession.Id); + AnsiConsole.MarkupLine($"[green]Coding session with id {codingSession.Id} has been deleted[/]"); + + } + } \ No newline at end of file diff --git a/Kerem.CodingTracker/Kerem.CodingTracker/Features/EditCodingSession/EditCodingSession.cs b/Kerem.CodingTracker/Kerem.CodingTracker/Features/EditCodingSession/EditCodingSession.cs index 35c060a44..9cfd6b8dd 100644 --- a/Kerem.CodingTracker/Kerem.CodingTracker/Features/EditCodingSession/EditCodingSession.cs +++ b/Kerem.CodingTracker/Kerem.CodingTracker/Features/EditCodingSession/EditCodingSession.cs @@ -17,7 +17,12 @@ public EditCodingSession(ICodingSessionRepository codingSessionRepository) public void CodingSessionEdit() { AnsiConsole.MarkupLine("[blue]Please enter the id of the coding session you want to edit[/]"); - var id = Convert.ToInt32(Console.ReadLine()); + int id = int.TryParse(Console.ReadLine(), out var selectedId) ? selectedId : 0; + if (id == 0) + { + AnsiConsole.MarkupLine("[red]The input its not a numerical value[/]"); + return; + } var codingSession = _codingSessionRepository.FindById(id); if (codingSession == null) { @@ -28,7 +33,7 @@ public void CodingSessionEdit() bool runProgram = true; while (runProgram) { - AnsiConsole.MarkupLine("[blue]Please select 1 to edit startime, 2 to edit end date or 3 to exit[/]"); + AnsiConsole.MarkupLine("[blue]Please select 1 to edit start date, 2 to edit end date or 3 to exit[/]"); int selectedProperty = Convert.ToInt32(Console.ReadLine()); switch (selectedProperty) { @@ -42,7 +47,6 @@ public void CodingSessionEdit() return; } - var correctFormat = Validator.ValidateDateFormat(startDate); @@ -52,12 +56,25 @@ public void CodingSessionEdit() return; } DateTime startTime = DateTime.Parse(startDate); - codingSession.StartTime = startTime; break; case 2: - AnsiConsole.MarkupLine("[blue]Enter a new end date[/]"); + AnsiConsole.MarkupLine("[blue]Please enter the end date in the format of yyyy-mm-dd hh:mm[/]"); var endDate = Console.ReadLine() ?? " "; + var emptyEndDate = string.IsNullOrEmpty(endDate); + if (emptyEndDate) + { + AnsiConsole.MarkupLine("[red]Date cannot be empty[/]"); + + return; + } + var correctFormatEndDate = Validator.ValidateDateFormat(endDate); + + if (!correctFormatEndDate) + { + AnsiConsole.MarkupLine("[red]Format is invalid[/]"); + return; + } DateTime endTime = DateTime.Parse(endDate); codingSession.EndTime = endTime; break; @@ -71,5 +88,7 @@ public void CodingSessionEdit() var minutes = (int)difference.TotalMinutes; codingSession.Duration = minutes; _codingSessionRepository.Save(codingSession); + AnsiConsole.MarkupLine("[green]Coding session has been updated[/]"); + } } \ No newline at end of file diff --git a/Kerem.CodingTracker/Kerem.CodingTracker/Infrastructure/Repositories/CodingSessionRepository.cs b/Kerem.CodingTracker/Kerem.CodingTracker/Infrastructure/Repositories/CodingSessionRepository.cs index 107227cb9..916a3a115 100644 --- a/Kerem.CodingTracker/Kerem.CodingTracker/Infrastructure/Repositories/CodingSessionRepository.cs +++ b/Kerem.CodingTracker/Kerem.CodingTracker/Infrastructure/Repositories/CodingSessionRepository.cs @@ -46,8 +46,15 @@ public CodingSession FindById(int id) public void Save(CodingSession codingSession) { var connection = _dapperDbContext.GetConnection(); - var sql = $"UPDATE CodingSession SET startTime = @StartTime, duration = @Duration WHERE Id = @Id"; - connection.ExecuteScalar(sql, new { codingSession.StartTime, codingSession.Duration, codingSession.Id }); + var sql = $"UPDATE CodingSession SET startTime = @StartTime, endTime = @EndTime, duration = @Duration WHERE Id = @Id"; + connection.ExecuteScalar(sql, new { codingSession.StartTime, codingSession.EndTime, codingSession.Duration, codingSession.Id }); + } + + public void Delete(int id) + { + var connection = _dapperDbContext.GetConnection(); + var sql = $"DELETE FROM CodingSession WHERE Id = @Id"; + connection.ExecuteScalar(sql, new { Id = id }); } diff --git a/Kerem.CodingTracker/Kerem.CodingTracker/Infrastructure/Utils/Validator.cs b/Kerem.CodingTracker/Kerem.CodingTracker/Infrastructure/Utils/Validator.cs index c08875c99..b720b5b43 100644 --- a/Kerem.CodingTracker/Kerem.CodingTracker/Infrastructure/Utils/Validator.cs +++ b/Kerem.CodingTracker/Kerem.CodingTracker/Infrastructure/Utils/Validator.cs @@ -1,4 +1,5 @@ using System.Text.RegularExpressions; +using Spectre.Console; namespace Kerem.CodingTracker.Features.CreateCodingSession ; @@ -22,5 +23,14 @@ public static bool Abort(string choice) } return false; } + + public static bool ValidateStartAndEndDate(DateTime startDate, DateTime endDate) + { + if (startDate > endDate) + { + return false; + } + return true; + } } \ No newline at end of file diff --git a/Kerem.CodingTracker/Kerem.CodingTracker/UI/ConsoleMenu.cs b/Kerem.CodingTracker/Kerem.CodingTracker/UI/ConsoleMenu.cs index 5d78d138e..5c52651b4 100644 --- a/Kerem.CodingTracker/Kerem.CodingTracker/UI/ConsoleMenu.cs +++ b/Kerem.CodingTracker/Kerem.CodingTracker/UI/ConsoleMenu.cs @@ -1,5 +1,6 @@ using Kerem.CodingTracker.Features; using Kerem.CodingTracker.Features.CreateCodingSession; +using Kerem.CodingTracker.Features.DeleteCodingSession; using Kerem.CodingTracker.Features.EditCodingSession; using Kerem.CodingTracker.Features.FindAllCodingSession; using Spectre.Console; @@ -12,13 +13,15 @@ public class ConsoleMenu private readonly CountCodingSession _countCodingSession; private readonly FindAllCodingSession _findAllCodingSession; private readonly EditCodingSession _editCodingSession; + private readonly DeleteCodingSession _deleteCodingSession; - public ConsoleMenu(CreateCodingSession createCodingSession, CountCodingSession countCodingSession, FindAllCodingSession findAllCodingSession, EditCodingSession editCodingSession ) + public ConsoleMenu(CreateCodingSession createCodingSession, CountCodingSession countCodingSession, FindAllCodingSession findAllCodingSession, EditCodingSession editCodingSession, DeleteCodingSession deleteCodingSession) { _createCodingSession = createCodingSession; _countCodingSession = countCodingSession; _findAllCodingSession = findAllCodingSession; _editCodingSession = editCodingSession; + _deleteCodingSession = deleteCodingSession; } public void Menu() @@ -50,6 +53,8 @@ public void Menu() _editCodingSession.CodingSessionEdit(); break; case '4': + _findAllCodingSession.FindAll(); + _deleteCodingSession.DeleteCodingSessionById(); break; case '5': AnsiConsole.MarkupLine("[bold green]Goodbye![/]"); diff --git a/Kerem.CodingTracker/NuGet.config b/Kerem.CodingTracker/NuGet.config new file mode 100644 index 000000000..765346e53 --- /dev/null +++ b/Kerem.CodingTracker/NuGet.config @@ -0,0 +1,7 @@ + + + + + + + diff --git a/Kerem.CodingTracker/README.md b/Kerem.CodingTracker/README.md new file mode 100644 index 000000000..ac6bb63c3 --- /dev/null +++ b/Kerem.CodingTracker/README.md @@ -0,0 +1,132 @@ +# Coding Tracker + +A console app for logging coding sessions — start time, end time, and an automatically +calculated duration — built as a follow-up to the Habit Logger project. This time the +focus is on handling dates/times correctly, using an external library (Dapper + +Spectre.Console), and applying Separation of Concerns instead of one flat `Program.cs`. + +## Features + +- Log a coding session by entering a start and end date/time +- View all logged sessions in a formatted table (Spectre.Console) +- Edit a session's start or end time +- Delete a session +- Duration is always derived from `EndTime - StartTime`, never entered by hand + +## Tech stack + +- .NET 8 / C# +- [Dapper](https://github.com/DapperLib/Dapper) for data access (no raw ADO.NET, no EF) +- [Spectre.Console](https://spectreconsole.net/) for all console output (tables, prompts, styled text) +- Microsoft.Extensions.DependencyInjection + Microsoft.Extensions.Configuration for DI and config +- SQL Server (via `Microsoft.Data.SqlClient`) as the backing store + +## Project structure + +The project is organized by architectural layer, with features further split into their +own folders so each operation (create/edit/delete/list/count) lives in its own file: + +``` +Kerem.CodingTracker/ + Domain/ + Entities/CodingSession.cs # the CodingSession model (Id, StartTime, EndTime, Duration) + Interfaces/ICodingSessionRepository.cs + Infrastructure/ + Persistance/DapperDbContext.cs # wraps the SqlConnection used by Dapper + Repositories/CodingSessionRepository.cs + Utils/Validator.cs # date format / abort / start-before-end checks + Features/ + CreateCodingSession/ + EditCodingSession/ + DeleteCodingSession/ + FindAllCodingSession/ + CountCodingSession/ + UI/ConsoleMenu.cs # the main menu loop + DependencyInjection.cs # wires everything up via IServiceCollection + Program.cs # entry point: builds config, builds DI, runs the menu + appsettings.json # connection string lives here, not hardcoded + +Kerem.CodingTracker.Tests/ + ValidatorTests.cs # unit tests for the pure validation methods +``` + +Each feature only depends on `ICodingSessionRepository`, never on the concrete +repository or Dapper directly, so the console/business logic stays decoupled from +the data access layer. + +## Why these design choices + +- **Feature folders over one big file**: each user action (create, edit, delete, list, + count) is its own class with a single public method, injected with only the + repository it needs. This maps directly to the Separation of Concerns requirement + and makes it obvious where to look when a specific menu option misbehaves. +- **`ICodingSessionRepository` interface**: the features depend on the interface, not + `CodingSessionRepository` directly, so the data access implementation could be swapped + (e.g. for a different database or a mock in tests) without touching feature code. +- **`Validator` as a static utility class**: date-format checking, the "abort" keyword + check, and the start-before-end check are pure functions with no console or database + dependency, so they're cheap to unit test in isolation (see Testing below) and reused + across `CreateCodingSession` and `EditCodingSession` instead of being duplicated. +- **Duration is never user input**: `CodingSession.Duration` is only ever set from + `(EndTime - StartTime).TotalMinutes`, calculated in the feature classes after both + dates have been validated. +- **Config over hardcoding**: the connection string lives in `appsettings.json` and is + read once in `Program.cs`, then passed into `DependencyInjection.AddApplication`, + so nothing in the data layer needs to know where the config file lives. + +## Date/time format + +The app only accepts dates typed in exactly this format: + +``` +yyyy-MM-dd HH:mm +``` + +Example: `2026-08-24 14:30` + +Anything else (wrong separators, missing leading zeros, a date with no time, etc.) is +rejected by `Validator.ValidateDateFormat` before it's parsed, and the end date is +checked against the start date (`Validator.ValidateStartAndEndDate`) so a session can't +end before it starts. + +## Getting started + +1. Make sure you have the .NET 8 SDK installed. +2. Update the `ConnectionStrings:DefaultConnection` value in + `Kerem.CodingTracker/appsettings.json` to point at a SQL Server instance you have + access to (default is `localhost\SQLEXPRESS` with integrated security). +3. Create the `CodingSession` table on that database: + + ```sql + CREATE TABLE CodingSession ( + Id INT IDENTITY(1,1) PRIMARY KEY, + StartTime DATETIME NOT NULL, + EndTime DATETIME NOT NULL, + Duration DECIMAL(10, 2) NOT NULL + ); + ``` + +4. Run the app from the `Kerem.CodingTracker` folder: + + ``` + dotnet run --project Kerem.CodingTracker + ``` + +## Running the tests + +``` +dotnet test +``` + +`Kerem.CodingTracker.Tests` covers the `Validator` methods — date format validation, +the "abort" keyword check, and the start-before-end check — since these are pure +functions with no I/O and are the easiest place for a date/time bug to hide. + +## Known limitations + +- The `CodingSession` table isn't created automatically; it needs to exist before the + app is run (see step 3 above). +- Numeric input (session IDs, edit menu selections) isn't guarded against non-numeric + input yet. +- No stopwatch-based live session tracking or filtering/sorting by period — these were + left as future improvements rather than implemented for this pass. From 303cee9d7ce44080a738a70784bb47f76373a86d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Kerem=20Bj=C3=A4lven=C3=A4s=20Tazedal?= <98690898+Kerem1989@users.noreply.github.com> Date: Mon, 24 Aug 2026 13:18:07 +0200 Subject: [PATCH 5/6] Added validation --- .../Features/EditCodingSession/EditCodingSession.cs | 5 ++++- .../Kerem.CodingTracker/Infrastructure/Utils/Validator.cs | 2 +- 2 files changed, 5 insertions(+), 2 deletions(-) diff --git a/Kerem.CodingTracker/Kerem.CodingTracker/Features/EditCodingSession/EditCodingSession.cs b/Kerem.CodingTracker/Kerem.CodingTracker/Features/EditCodingSession/EditCodingSession.cs index 9cfd6b8dd..a3988fe3f 100644 --- a/Kerem.CodingTracker/Kerem.CodingTracker/Features/EditCodingSession/EditCodingSession.cs +++ b/Kerem.CodingTracker/Kerem.CodingTracker/Features/EditCodingSession/EditCodingSession.cs @@ -34,9 +34,12 @@ public void CodingSessionEdit() while (runProgram) { AnsiConsole.MarkupLine("[blue]Please select 1 to edit start date, 2 to edit end date or 3 to exit[/]"); - int selectedProperty = Convert.ToInt32(Console.ReadLine()); + int selectedProperty = int.TryParse(Console.ReadLine(), out var selected) ? selected : 0; switch (selectedProperty) { + case 0: + AnsiConsole.MarkupLine("The selected choice was a incorrect value"); + break; case 1: AnsiConsole.MarkupLine("[bold steelblue]Please enter the start date in the format of yyyy-mm-dd hh:mm[/]"); var startDate = Console.ReadLine() ?? " "; diff --git a/Kerem.CodingTracker/Kerem.CodingTracker/Infrastructure/Utils/Validator.cs b/Kerem.CodingTracker/Kerem.CodingTracker/Infrastructure/Utils/Validator.cs index b720b5b43..0bf02a70f 100644 --- a/Kerem.CodingTracker/Kerem.CodingTracker/Infrastructure/Utils/Validator.cs +++ b/Kerem.CodingTracker/Kerem.CodingTracker/Infrastructure/Utils/Validator.cs @@ -7,7 +7,7 @@ public static class Validator { public static bool ValidateDateFormat(String date) { - string pattern = @"^\d{4}-\d{2}-\d{2} \d{2}:\d{2}$"; + string pattern = @"^\d{4}-(0[1-9]|1[0-2])-(0[1-9]|[12]\d|3[01]) ([01]\d|2[0-3]):[0-5]\d$"; if (Regex.IsMatch(date, pattern)) { return true; From e2a57bec93883e983d5cff0e742e64406b9b9d46 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Kerem=20Bj=C3=A4lven=C3=A4s=20Tazedal?= <98690898+Kerem1989@users.noreply.github.com> Date: Mon, 24 Aug 2026 13:27:45 +0200 Subject: [PATCH 6/6] Updated readme --- Kerem.CodingTracker/README.md | 17 +++-------------- 1 file changed, 3 insertions(+), 14 deletions(-) diff --git a/Kerem.CodingTracker/README.md b/Kerem.CodingTracker/README.md index ac6bb63c3..faaee4f21 100644 --- a/Kerem.CodingTracker/README.md +++ b/Kerem.CodingTracker/README.md @@ -1,7 +1,7 @@ # Coding Tracker -A console app for logging coding sessions — start time, end time, and an automatically -calculated duration — built as a follow-up to the Habit Logger project. This time the +A console app for logging coding sessions, start time, end time, and an automatically +calculated duration, built as a follow-up to the Habit Logger project. This time the focus is on handling dates/times correctly, using an external library (Dapper + Spectre.Console), and applying Separation of Concerns instead of one flat `Program.cs`. @@ -118,15 +118,4 @@ end before it starts. dotnet test ``` -`Kerem.CodingTracker.Tests` covers the `Validator` methods — date format validation, -the "abort" keyword check, and the start-before-end check — since these are pure -functions with no I/O and are the easiest place for a date/time bug to hide. - -## Known limitations - -- The `CodingSession` table isn't created automatically; it needs to exist before the - app is run (see step 3 above). -- Numeric input (session IDs, edit menu selections) isn't guarded against non-numeric - input yet. -- No stopwatch-based live session tracking or filtering/sorting by period — these were - left as future improvements rather than implemented for this pass. +`Kerem.CodingTracker.Tests` covers the `Validator` methods