From f2e599c281d9aff193b892026db9fcbaecdd2d5e Mon Sep 17 00:00:00 2001 From: Kelvin Tawiah Date: Thu, 15 Jan 2026 16:55:12 -0600 Subject: [PATCH 01/13] initialize project --- .vscode/settings.json | 3 +++ .../KelvinTawiah.CodingTracker.csproj | 15 +++++++++++++++ KelvinTawiah.CodingTracker/Program.cs | 4 ++++ KelvinTawiah.slnx | 3 +++ 4 files changed, 25 insertions(+) create mode 100644 .vscode/settings.json create mode 100644 KelvinTawiah.CodingTracker/KelvinTawiah.CodingTracker.csproj create mode 100644 KelvinTawiah.CodingTracker/Program.cs create mode 100644 KelvinTawiah.slnx diff --git a/.vscode/settings.json b/.vscode/settings.json new file mode 100644 index 00000000..1b3fdd24 --- /dev/null +++ b/.vscode/settings.json @@ -0,0 +1,3 @@ +{ + "dotnet.defaultSolution": "KelvinTawiah.slnx" +} diff --git a/KelvinTawiah.CodingTracker/KelvinTawiah.CodingTracker.csproj b/KelvinTawiah.CodingTracker/KelvinTawiah.CodingTracker.csproj new file mode 100644 index 00000000..b24afb2d --- /dev/null +++ b/KelvinTawiah.CodingTracker/KelvinTawiah.CodingTracker.csproj @@ -0,0 +1,15 @@ + + + + Exe + net10.0 + enable + enable + + + + + + + + diff --git a/KelvinTawiah.CodingTracker/Program.cs b/KelvinTawiah.CodingTracker/Program.cs new file mode 100644 index 00000000..912a4ffb --- /dev/null +++ b/KelvinTawiah.CodingTracker/Program.cs @@ -0,0 +1,4 @@ +public class Program +{ + public static void Main(string[] args) { } +} \ No newline at end of file diff --git a/KelvinTawiah.slnx b/KelvinTawiah.slnx new file mode 100644 index 00000000..d26ca584 --- /dev/null +++ b/KelvinTawiah.slnx @@ -0,0 +1,3 @@ + + + From fd85f411f549d78d65eaed6a58e6e106becdc2ef Mon Sep 17 00:00:00 2001 From: Kelvin Tawiah Date: Thu, 15 Jan 2026 16:57:19 -0600 Subject: [PATCH 02/13] feat: Add project structure (models, repos, services) --- .gitignore | 1 + KelvinTawiah.CodingTracker/Requirements.md | 33 +++++++++++++++++++ .../appsettions.example.json | 8 +++++ KelvinTawiah.CodingTracker/model/Session.cs | 7 ++++ .../model/SessionLog.cs | 5 +++ .../repository/SessionLogRepository.cs | 22 +++++++++++++ .../repository/SessionRepository.cs | 28 ++++++++++++++++ .../service/SessionLogService.cs | 22 +++++++++++++ .../service/SessionService.cs | 27 +++++++++++++++ 9 files changed, 153 insertions(+) create mode 100644 KelvinTawiah.CodingTracker/Requirements.md create mode 100644 KelvinTawiah.CodingTracker/appsettions.example.json create mode 100644 KelvinTawiah.CodingTracker/model/Session.cs create mode 100644 KelvinTawiah.CodingTracker/model/SessionLog.cs create mode 100644 KelvinTawiah.CodingTracker/repository/SessionLogRepository.cs create mode 100644 KelvinTawiah.CodingTracker/repository/SessionRepository.cs create mode 100644 KelvinTawiah.CodingTracker/service/SessionLogService.cs create mode 100644 KelvinTawiah.CodingTracker/service/SessionService.cs diff --git a/.gitignore b/.gitignore index 49976e11..b6dea80a 100644 --- a/.gitignore +++ b/.gitignore @@ -478,3 +478,4 @@ $RECYCLE.BIN/ *.lnk /MathGame2 /CodingTracker.TomDonegan/TextFile1.txt +KelvinTawiah.CodingTracker/appsettings.json diff --git a/KelvinTawiah.CodingTracker/Requirements.md b/KelvinTawiah.CodingTracker/Requirements.md new file mode 100644 index 00000000..08c9dd9e --- /dev/null +++ b/KelvinTawiah.CodingTracker/Requirements.md @@ -0,0 +1,33 @@ +Requirements + +This application has the same requirements as the previous project, except that now you'll be logging your daily coding time. + + +To show the data on the console, you should use the Spectre.Console library. + + +You're required to have separate classes in different files (i.e. UserInput.cs, Validation.cs, CodingController.cs) + + +You should tell the user the specific format you want the date and time to be logged and not allow any other format. + + +You'll need to create a configuration file called appsettings.json, which will contain your database path and connection strings (and any other configs you might need). + + +You'll need to create a CodingSession class in a separate file. It will contain the properties of your coding session: Id, StartTime, EndTime, Duration. When reading from the database, you can't use an anonymous object, you have to read your table into a List of CodingSession. + + +The user shouldn't input the duration of the session. It should be calculated based on the Start and End times + + +The user should be able to input the start and end times manually. + + +You need to use Dapper ORM for the data access instead of ADO.NET. (This requirement was included in Feb/2024) + + +Follow the DRY Principle, and avoid code repetition. + + +Don't forget the ReadMe explaining your thought process. \ No newline at end of file diff --git a/KelvinTawiah.CodingTracker/appsettions.example.json b/KelvinTawiah.CodingTracker/appsettions.example.json new file mode 100644 index 00000000..f041a2b6 --- /dev/null +++ b/KelvinTawiah.CodingTracker/appsettions.example.json @@ -0,0 +1,8 @@ +{ + "Database": { + "Name": "", + "User": "", + "Password": "", + "Server": "" + } +} diff --git a/KelvinTawiah.CodingTracker/model/Session.cs b/KelvinTawiah.CodingTracker/model/Session.cs new file mode 100644 index 00000000..f8dca850 --- /dev/null +++ b/KelvinTawiah.CodingTracker/model/Session.cs @@ -0,0 +1,7 @@ +internal class Session +{ + public int Id { get; private set; } + public DateTime StartTime { get; private set; } + public DateTime EndTime { get; private set; } + public string Notes { get; set; } = string.Empty; +} \ No newline at end of file diff --git a/KelvinTawiah.CodingTracker/model/SessionLog.cs b/KelvinTawiah.CodingTracker/model/SessionLog.cs new file mode 100644 index 00000000..d9c652ca --- /dev/null +++ b/KelvinTawiah.CodingTracker/model/SessionLog.cs @@ -0,0 +1,5 @@ +class SessionLog +{ + public int Id { get; private set; } + public Session? SessionDetails { get; set; } +} \ No newline at end of file diff --git a/KelvinTawiah.CodingTracker/repository/SessionLogRepository.cs b/KelvinTawiah.CodingTracker/repository/SessionLogRepository.cs new file mode 100644 index 00000000..8d68b092 --- /dev/null +++ b/KelvinTawiah.CodingTracker/repository/SessionLogRepository.cs @@ -0,0 +1,22 @@ +class SessionLogRepository +{ + public void Add(SessionLog log) + { + + } + + public void Clear() + { + + } + + public List View() + { + throw new NotImplementedException(); + } + + public List FindBy(int Id) + { + throw new NotImplementedException(); + } +} \ No newline at end of file diff --git a/KelvinTawiah.CodingTracker/repository/SessionRepository.cs b/KelvinTawiah.CodingTracker/repository/SessionRepository.cs new file mode 100644 index 00000000..87b2b907 --- /dev/null +++ b/KelvinTawiah.CodingTracker/repository/SessionRepository.cs @@ -0,0 +1,28 @@ +class SessionRepository +{ + + public void Create(Session session) + { + + } + + public Session FindById(int id) + { + throw new NotImplementedException(); + } + + public void Update(int id) + { + + } + + public List GetAll() + { + throw new NotImplementedException(); + } + + public void Delete(int id) + { + + } +} \ No newline at end of file diff --git a/KelvinTawiah.CodingTracker/service/SessionLogService.cs b/KelvinTawiah.CodingTracker/service/SessionLogService.cs new file mode 100644 index 00000000..1d3d106f --- /dev/null +++ b/KelvinTawiah.CodingTracker/service/SessionLogService.cs @@ -0,0 +1,22 @@ +class SessionLogService +{ + public void AddLog(SessionLog log) + { + + } + + public void ClearLog() + { + + } + + public List ViewLogs() + { + throw new NotImplementedException(); + } + + public List ViewLogsFor(int Id) + { + throw new NotImplementedException(); + } +} \ No newline at end of file diff --git a/KelvinTawiah.CodingTracker/service/SessionService.cs b/KelvinTawiah.CodingTracker/service/SessionService.cs new file mode 100644 index 00000000..8cdd852e --- /dev/null +++ b/KelvinTawiah.CodingTracker/service/SessionService.cs @@ -0,0 +1,27 @@ +class SessionService +{ + public void AddSession(Session session) + { + + } + + public Session GetSessionById(int id) + { + throw new NotImplementedException(); + } + + public void UpdateSession(int id) + { + + } + + public List GetAllSessions() + { + throw new NotImplementedException(); + } + + public void DeleteSession(int id) + { + + } +} \ No newline at end of file From 946518675df9475df17b62e243270b909708b3cc Mon Sep 17 00:00:00 2001 From: Kelvin Tawiah Date: Fri, 16 Jan 2026 01:00:29 -0600 Subject: [PATCH 03/13] Add Dapper package dependency --- KelvinTawiah.CodingTracker/KelvinTawiah.CodingTracker.csproj | 1 + 1 file changed, 1 insertion(+) diff --git a/KelvinTawiah.CodingTracker/KelvinTawiah.CodingTracker.csproj b/KelvinTawiah.CodingTracker/KelvinTawiah.CodingTracker.csproj index b24afb2d..6d350b4c 100644 --- a/KelvinTawiah.CodingTracker/KelvinTawiah.CodingTracker.csproj +++ b/KelvinTawiah.CodingTracker/KelvinTawiah.CodingTracker.csproj @@ -8,6 +8,7 @@ + From 810ec285c721a777c673a44984373b3c52da07b3 Mon Sep 17 00:00:00 2001 From: Kelvin Tawiah Date: Fri, 16 Jan 2026 01:00:42 -0600 Subject: [PATCH 04/13] Update domain models: add DurationMinutes, refactor SessionLog schema --- KelvinTawiah.CodingTracker/model/Session.cs | 11 +++++++---- KelvinTawiah.CodingTracker/model/SessionLog.cs | 11 ++++++++--- 2 files changed, 15 insertions(+), 7 deletions(-) diff --git a/KelvinTawiah.CodingTracker/model/Session.cs b/KelvinTawiah.CodingTracker/model/Session.cs index f8dca850..33765c37 100644 --- a/KelvinTawiah.CodingTracker/model/Session.cs +++ b/KelvinTawiah.CodingTracker/model/Session.cs @@ -1,7 +1,10 @@ -internal class Session +namespace KelvinTawiah.CodingTracker.model; + +public class Session { - public int Id { get; private set; } - public DateTime StartTime { get; private set; } - public DateTime EndTime { get; private set; } + public int Id { get; set; } + public DateTime StartTime { get; set; } + public DateTime EndTime { get; set; } + public int DurationMinutes { get; set; } public string Notes { get; set; } = string.Empty; } \ No newline at end of file diff --git a/KelvinTawiah.CodingTracker/model/SessionLog.cs b/KelvinTawiah.CodingTracker/model/SessionLog.cs index d9c652ca..5c4c6f0a 100644 --- a/KelvinTawiah.CodingTracker/model/SessionLog.cs +++ b/KelvinTawiah.CodingTracker/model/SessionLog.cs @@ -1,5 +1,10 @@ -class SessionLog +namespace KelvinTawiah.CodingTracker.model; + +public class SessionLog { - public int Id { get; private set; } - public Session? SessionDetails { get; set; } + public int Id { get; set; } + public int? SessionId { get; set; } + public string Action { get; set; } = string.Empty; + public string Message { get; set; } = string.Empty; + public DateTime LoggedAt { get; set; } } \ No newline at end of file From 90e401898cb197312606019a2a3e2bf1596f4ed7 Mon Sep 17 00:00:00 2001 From: Kelvin Tawiah Date: Fri, 16 Jan 2026 01:00:56 -0600 Subject: [PATCH 05/13] Add Validation class for date format enforcement --- KelvinTawiah.CodingTracker/Validation.cs | 21 +++++++++++++++++++++ 1 file changed, 21 insertions(+) create mode 100644 KelvinTawiah.CodingTracker/Validation.cs diff --git a/KelvinTawiah.CodingTracker/Validation.cs b/KelvinTawiah.CodingTracker/Validation.cs new file mode 100644 index 00000000..0c0daae7 --- /dev/null +++ b/KelvinTawiah.CodingTracker/Validation.cs @@ -0,0 +1,21 @@ +using System.Globalization; + +namespace KelvinTawiah.CodingTracker; + +public static class Validation +{ + public const string DateFormat = "yyyy-MM-dd HH:mm"; + + public static bool TryParseDateTime(string input, out DateTime value) + { + return DateTime.TryParseExact( + input, + DateFormat, + CultureInfo.InvariantCulture, + DateTimeStyles.None, + out value + ); + } + + public static bool IsEndAfterStart(DateTime start, DateTime end) => end > start; +} From 61743bbdd73dd217bd62d9441412b8bae14693db Mon Sep 17 00:00:00 2001 From: Kelvin Tawiah Date: Fri, 16 Jan 2026 01:01:14 -0600 Subject: [PATCH 06/13] Add UserInput class with Spectre.Console prompts --- KelvinTawiah.CodingTracker/UserInput.cs | 88 +++++++++++++++++++++++++ 1 file changed, 88 insertions(+) create mode 100644 KelvinTawiah.CodingTracker/UserInput.cs diff --git a/KelvinTawiah.CodingTracker/UserInput.cs b/KelvinTawiah.CodingTracker/UserInput.cs new file mode 100644 index 00000000..83a4ded9 --- /dev/null +++ b/KelvinTawiah.CodingTracker/UserInput.cs @@ -0,0 +1,88 @@ +using Spectre.Console; +using KelvinTawiah.CodingTracker.model; + +namespace KelvinTawiah.CodingTracker; + +public static class UserInput +{ + public static Session PromptForSession() + { + var start = PromptDate("start time"); + var end = PromptEndDate(start); + var notes = AnsiConsole.Prompt( + new TextPrompt("Notes (optional):") + .AllowEmpty() + ); + + var duration = (int)(end - start).TotalMinutes; + + return new Session + { + StartTime = start, + EndTime = end, + DurationMinutes = duration, + Notes = notes + }; + } + + public static Session PromptForSessionUpdate(Session existing) + { + var start = PromptDate("new start time", existing.StartTime); + var end = PromptEndDate(start, existing.EndTime); + var notes = AnsiConsole.Prompt( + new TextPrompt("Notes (optional):") + .DefaultValue(existing.Notes) + .AllowEmpty() + ); + + var duration = (int)(end - start).TotalMinutes; + + existing.StartTime = start; + existing.EndTime = end; + existing.DurationMinutes = duration; + existing.Notes = notes; + return existing; + } + + public static int PromptForSessionId(string message) + { + return AnsiConsole.Prompt( + new TextPrompt(message) + .Validate(id => id > 0 ? ValidationResult.Success() : ValidationResult.Error("Id must be positive")) + ); + } + + private static DateTime PromptDate(string label, DateTime? defaultValue = null) + { + while (true) + { + var prompt = new TextPrompt($"Enter {label} ({Validation.DateFormat}):") + .PromptStyle("green"); + + if (defaultValue.HasValue) + { + prompt = prompt.DefaultValue(defaultValue.Value.ToString(Validation.DateFormat)); + } + + var input = AnsiConsole.Prompt(prompt); + + if (Validation.TryParseDateTime(input, out var dt)) + { + return dt; + } + + AnsiConsole.MarkupLine("[red]Invalid format. Please use {0}[/]", Validation.DateFormat); + } + } + + private static DateTime PromptEndDate(DateTime start, DateTime? defaultValue = null) + { + while (true) + { + var end = PromptDate("end time", defaultValue); + if (Validation.IsEndAfterStart(start, end)) return end; + + AnsiConsole.MarkupLine("[red]End time must be after start time.[/]"); + } + } +} From 798f39cdbbdc9d3d65991f06e901a60e1f4c9048 Mon Sep 17 00:00:00 2001 From: Kelvin Tawiah Date: Fri, 16 Jan 2026 01:01:30 -0600 Subject: [PATCH 07/13] Implement Dapper repositories for Session and SessionLog --- .../repository/SessionLogRepository.cs | 39 ++++++++++++-- .../repository/SessionRepository.cs | 53 ++++++++++++++++--- 2 files changed, 82 insertions(+), 10 deletions(-) diff --git a/KelvinTawiah.CodingTracker/repository/SessionLogRepository.cs b/KelvinTawiah.CodingTracker/repository/SessionLogRepository.cs index 8d68b092..1b61d292 100644 --- a/KelvinTawiah.CodingTracker/repository/SessionLogRepository.cs +++ b/KelvinTawiah.CodingTracker/repository/SessionLogRepository.cs @@ -1,22 +1,53 @@ -class SessionLogRepository +using Microsoft.Data.SqlClient; +using Dapper; +using KelvinTawiah.CodingTracker.model; + +namespace KelvinTawiah.CodingTracker.repository; + +public class SessionLogRepository { + private readonly string _connectionString; + + public SessionLogRepository(string connectionString) + { + _connectionString = connectionString; + } + public void Add(SessionLog log) { + using var connection = new SqlConnection(_connectionString); + var query = @" + INSERT INTO SessionLogs (SessionId, Action, Message, LoggedAt) + VALUES (@SessionId, @Action, @Message, @LoggedAt)"; + + connection.Execute(query, log); } public void Clear() { + using var connection = new SqlConnection(_connectionString); + + var query = "DELETE FROM SessionLogs"; + connection.Execute(query); } public List View() { - throw new NotImplementedException(); + using var connection = new SqlConnection(_connectionString); + + var query = "SELECT * FROM SessionLogs ORDER BY LoggedAt DESC"; + + return connection.Query(query).ToList(); } - public List FindBy(int Id) + public List FindBy(int id) { - throw new NotImplementedException(); + using var connection = new SqlConnection(_connectionString); + + var query = "SELECT * FROM SessionLogs WHERE SessionId = @Id ORDER BY LoggedAt DESC"; + + return connection.Query(query, new { Id = id }).ToList(); } } \ No newline at end of file diff --git a/KelvinTawiah.CodingTracker/repository/SessionRepository.cs b/KelvinTawiah.CodingTracker/repository/SessionRepository.cs index 87b2b907..1d226146 100644 --- a/KelvinTawiah.CodingTracker/repository/SessionRepository.cs +++ b/KelvinTawiah.CodingTracker/repository/SessionRepository.cs @@ -1,28 +1,69 @@ -class SessionRepository +using Microsoft.Data.SqlClient; +using Dapper; +using KelvinTawiah.CodingTracker.model; + +namespace KelvinTawiah.CodingTracker.repository; + +public class SessionRepository { + private readonly string _connectionString; + + public SessionRepository(string connectionString) + { + _connectionString = connectionString; + } - public void Create(Session session) + public int Create(Session session) { + using var connection = new SqlConnection(_connectionString); + + var query = @" + INSERT INTO CodingSessions (StartTime, EndTime, DurationMinutes, Notes) + OUTPUT INSERTED.Id + VALUES (@StartTime, @EndTime, @DurationMinutes, @Notes)"; + return connection.ExecuteScalar(query, session); } - public Session FindById(int id) + public Session? FindById(int id) { - throw new NotImplementedException(); + using var connection = new SqlConnection(_connectionString); + + var query = "SELECT * FROM CodingSessions WHERE Id = @Id"; + + return connection.QueryFirstOrDefault(query, new { Id = id }); } - public void Update(int id) + public void Update(Session session) { + using var connection = new SqlConnection(_connectionString); + + var query = @" + UPDATE CodingSessions + SET StartTime = @StartTime, + EndTime = @EndTime, + DurationMinutes = @DurationMinutes, + Notes = @Notes + WHERE Id = @Id"; + connection.Execute(query, session); } public List GetAll() { - throw new NotImplementedException(); + using var connection = new SqlConnection(_connectionString); + + var query = "SELECT * FROM CodingSessions"; + + return connection.Query(query).ToList(); } public void Delete(int id) { + using var connection = new SqlConnection(_connectionString); + + var query = "DELETE FROM CodingSessions WHERE Id = @Id"; + connection.Execute(query, new { Id = id }); } } \ No newline at end of file From 87240b6454b90973966ee15fd87727f133a7a89e Mon Sep 17 00:00:00 2001 From: Kelvin Tawiah Date: Fri, 16 Jan 2026 01:01:42 -0600 Subject: [PATCH 08/13] Implement service layer with duration calculation --- .../service/SessionLogService.cs | 24 ++++++++++---- .../service/SessionService.cs | 31 ++++++++++++++----- 2 files changed, 41 insertions(+), 14 deletions(-) diff --git a/KelvinTawiah.CodingTracker/service/SessionLogService.cs b/KelvinTawiah.CodingTracker/service/SessionLogService.cs index 1d3d106f..a93f4883 100644 --- a/KelvinTawiah.CodingTracker/service/SessionLogService.cs +++ b/KelvinTawiah.CodingTracker/service/SessionLogService.cs @@ -1,22 +1,34 @@ -class SessionLogService +using KelvinTawiah.CodingTracker.model; +using KelvinTawiah.CodingTracker.repository; + +namespace KelvinTawiah.CodingTracker.service; + +public class SessionLogService { - public void AddLog(SessionLog log) + private readonly SessionLogRepository _repository; + + public SessionLogService(string connectionString) { + _repository = new SessionLogRepository(connectionString); + } + public void AddLog(SessionLog log) + { + _repository.Add(log); } public void ClearLog() { - + _repository.Clear(); } public List ViewLogs() { - throw new NotImplementedException(); + return _repository.View(); } - public List ViewLogsFor(int Id) + public List ViewLogsFor(int id) { - throw new NotImplementedException(); + return _repository.FindBy(id); } } \ No newline at end of file diff --git a/KelvinTawiah.CodingTracker/service/SessionService.cs b/KelvinTawiah.CodingTracker/service/SessionService.cs index 8cdd852e..d3f5fc3e 100644 --- a/KelvinTawiah.CodingTracker/service/SessionService.cs +++ b/KelvinTawiah.CodingTracker/service/SessionService.cs @@ -1,27 +1,42 @@ -class SessionService +using KelvinTawiah.CodingTracker.model; +using KelvinTawiah.CodingTracker.repository; + +namespace KelvinTawiah.CodingTracker.service; + +public class SessionService { - public void AddSession(Session session) - { + private readonly SessionRepository _repository; + public SessionService(string connectionString) + { + _repository = new SessionRepository(connectionString); } - public Session GetSessionById(int id) + public void AddSession(Session session) { - throw new NotImplementedException(); + session.DurationMinutes = (int)(session.EndTime - session.StartTime).TotalMinutes; + var id = _repository.Create(session); + session.Id = id; } - public void UpdateSession(int id) + public Session? GetSessionById(int id) { + return _repository.FindById(id); + } + public void UpdateSession(Session session) + { + session.DurationMinutes = (int)(session.EndTime - session.StartTime).TotalMinutes; + _repository.Update(session); } public List GetAllSessions() { - throw new NotImplementedException(); + return _repository.GetAll(); } public void DeleteSession(int id) { - + _repository.Delete(id); } } \ No newline at end of file From 4fef006143ac46aa8d9863d2614b2bf2ea684dda Mon Sep 17 00:00:00 2001 From: Kelvin Tawiah Date: Fri, 16 Jan 2026 01:01:56 -0600 Subject: [PATCH 09/13] Add CodingController with Spectre.Console UI --- .../CodingController.cs | 209 ++++++++++++++++++ 1 file changed, 209 insertions(+) create mode 100644 KelvinTawiah.CodingTracker/CodingController.cs diff --git a/KelvinTawiah.CodingTracker/CodingController.cs b/KelvinTawiah.CodingTracker/CodingController.cs new file mode 100644 index 00000000..2aec88b7 --- /dev/null +++ b/KelvinTawiah.CodingTracker/CodingController.cs @@ -0,0 +1,209 @@ +using Spectre.Console; +using KelvinTawiah.CodingTracker.model; +using KelvinTawiah.CodingTracker.service; + +namespace KelvinTawiah.CodingTracker; + +public class CodingController +{ + private readonly SessionService _sessionService; + private readonly SessionLogService _logService; + + public CodingController(SessionService sessionService, SessionLogService logService) + { + _sessionService = sessionService; + _logService = logService; + } + + public void Run() + { + while (true) + { + var choice = AnsiConsole.Prompt( + new SelectionPrompt() + .Title("[bold blue]Coding Tracker[/]") + .PageSize(10) + .AddChoices( + "Add session", + "View all sessions", + "View session details", + "Update session", + "Delete session", + "View logs", + "Exit") + ); + + switch (choice) + { + case "Add session": + AddSession(); + break; + case "View all sessions": + ShowAllSessions(); + break; + case "View session details": + ShowSessionDetails(); + break; + case "Update session": + UpdateSession(); + break; + case "Delete session": + DeleteSession(); + break; + case "View logs": + ShowLogs(); + break; + case "Exit": + return; + } + } + } + + private void AddSession() + { + var session = UserInput.PromptForSession(); + _sessionService.AddSession(session); + _logService.AddLog(new SessionLog + { + SessionId = session.Id, + Action = "Create", + Message = "Session added", + LoggedAt = DateTime.Now + }); + + AnsiConsole.MarkupLine("[green]Session added successfully.[/]"); + WaitForKey(); + } + + private void ShowAllSessions() + { + var sessions = _sessionService.GetAllSessions(); + var table = new Table().RoundedBorder(); + table.AddColumn("Id"); + table.AddColumn("Start"); + table.AddColumn("End"); + table.AddColumn("Duration (min)"); + table.AddColumn("Notes"); + + foreach (var s in sessions) + { + table.AddRow( + s.Id.ToString(), + s.StartTime.ToString(Validation.DateFormat), + s.EndTime.ToString(Validation.DateFormat), + s.DurationMinutes.ToString(), + string.IsNullOrWhiteSpace(s.Notes) ? "-" : s.Notes + ); + } + + if (sessions.Count == 0) + { + AnsiConsole.MarkupLine("[yellow]No sessions found.[/]"); + } + else + { + AnsiConsole.Write(table); + } + + WaitForKey(); + } + + private void ShowSessionDetails() + { + var id = UserInput.PromptForSessionId("Enter session Id:"); + var session = _sessionService.GetSessionById(id); + if (session == null) + { + AnsiConsole.MarkupLine("[red]Session not found.[/]"); + WaitForKey(); + return; + } + + var panel = new Panel($"Id: {session.Id}\nStart: {session.StartTime:yyyy-MM-dd HH:mm}\nEnd: {session.EndTime:yyyy-MM-dd HH:mm}\nDuration (min): {session.DurationMinutes}\nNotes: {session.Notes}") + .Border(BoxBorder.Rounded) + .Header("Session Details", Justify.Center); + + AnsiConsole.Write(panel); + WaitForKey(); + } + + private void UpdateSession() + { + var id = UserInput.PromptForSessionId("Enter session Id:"); + var session = _sessionService.GetSessionById(id); + if (session == null) + { + AnsiConsole.MarkupLine("[red]Session not found.[/]"); + WaitForKey(); + return; + } + + var updated = UserInput.PromptForSessionUpdate(session); + _sessionService.UpdateSession(updated); + _logService.AddLog(new SessionLog + { + SessionId = updated.Id, + Action = "Update", + Message = "Session updated", + LoggedAt = DateTime.Now + }); + + AnsiConsole.MarkupLine("[green]Session updated.[/]"); + WaitForKey(); + } + + private void DeleteSession() + { + var id = UserInput.PromptForSessionId("Enter session Id:"); + _sessionService.DeleteSession(id); + _logService.AddLog(new SessionLog + { + SessionId = id, + Action = "Delete", + Message = "Session deleted", + LoggedAt = DateTime.Now + }); + + AnsiConsole.MarkupLine("[green]Session deleted.[/]"); + WaitForKey(); + } + + private void ShowLogs() + { + var logs = _logService.ViewLogs(); + var table = new Table().RoundedBorder(); + table.AddColumn("Id"); + table.AddColumn("SessionId"); + table.AddColumn("Action"); + table.AddColumn("Message"); + table.AddColumn("LoggedAt"); + + foreach (var log in logs) + { + table.AddRow( + log.Id.ToString(), + log.SessionId?.ToString() ?? "-", + log.Action, + string.IsNullOrWhiteSpace(log.Message) ? "-" : log.Message, + log.LoggedAt.ToString(Validation.DateFormat) + ); + } + + if (logs.Count == 0) + { + AnsiConsole.MarkupLine("[yellow]No logs found.[/]"); + } + else + { + AnsiConsole.Write(table); + } + + WaitForKey(); + } + + private static void WaitForKey() + { + AnsiConsole.MarkupLine("\n[grey]Press any key to continue...[/]"); + Console.ReadKey(); + } +} From 7e711e59450554cb211f5a8cf6280a799ffb0f6a Mon Sep 17 00:00:00 2001 From: Kelvin Tawiah Date: Fri, 16 Jan 2026 01:02:12 -0600 Subject: [PATCH 10/13] Update Program.cs with database initialization and controller integration --- KelvinTawiah.CodingTracker/Program.cs | 117 +++++++++++++++++++++++++- 1 file changed, 115 insertions(+), 2 deletions(-) diff --git a/KelvinTawiah.CodingTracker/Program.cs b/KelvinTawiah.CodingTracker/Program.cs index 912a4ffb..11667aa9 100644 --- a/KelvinTawiah.CodingTracker/Program.cs +++ b/KelvinTawiah.CodingTracker/Program.cs @@ -1,4 +1,117 @@ -public class Program +using System.Text.Json; +using Microsoft.Data.SqlClient; +using KelvinTawiah.CodingTracker; +using KelvinTawiah.CodingTracker.service; + +public class Program { - public static void Main(string[] args) { } + static readonly string APP_SETTING_PATH = FindPathUpward("appsettings.json"); + static readonly string MASTER_DB_USER = "master"; + static string? DATABASE_NAME; + static string? DATABASE_PASSWORD; + static string? DATABASE_USER; + static string? DATABASE_SERVER; + static string? CONNECTION_STRING; + + + private static string FindPathUpward(string fileName) + { + string path = fileName; + + DirectoryInfo? currDir = new(Directory.GetCurrentDirectory()); + + while (currDir != null) + { + string potentialPath = Path.Combine(currDir.FullName, fileName); + + if (File.Exists(potentialPath)) return potentialPath; + currDir = currDir.Parent; + } + + return path; + } + + public static void Main(string[] args) + { + var appSettings = LoadAppSettings(); + DATABASE_NAME = appSettings.GetProperty("Database").GetProperty("Name").GetString(); + DATABASE_USER = appSettings.GetProperty("Database").GetProperty("User").GetString(); + DATABASE_PASSWORD = appSettings.GetProperty("Database").GetProperty("Password").GetString(); + DATABASE_SERVER = appSettings.GetProperty("Database").GetProperty("Server").GetString(); + + InitializeDatabase(); + CONNECTION_STRING = $"Server={DATABASE_SERVER};Database={DATABASE_NAME};User Id={DATABASE_USER};Password={DATABASE_PASSWORD};TrustServerCertificate=True"; + + var sessionService = new SessionService(CONNECTION_STRING!); + var sessionLogService = new SessionLogService(CONNECTION_STRING!); + var controller = new CodingController(sessionService, sessionLogService); + + controller.Run(); + } + + private static void InitializeDatabase() + { + var masterConnString = $"Server={DATABASE_SERVER};Database={MASTER_DB_USER};User Id={DATABASE_USER};Password={DATABASE_PASSWORD};TrustServerCertificate=True"; + var appConnString = $"Server={DATABASE_SERVER};Database={DATABASE_NAME};User Id={DATABASE_USER};Password={DATABASE_PASSWORD};TrustServerCertificate=True"; + + using (var conn = new SqlConnection(masterConnString)) + { + conn.Open(); + var cmd = conn.CreateCommand(); + cmd.CommandText = $"IF DB_ID('{DATABASE_NAME}') IS NULL CREATE DATABASE {DATABASE_NAME}"; + cmd.ExecuteNonQuery(); + } + + // Create tables + using (var conn = new SqlConnection(appConnString)) + { + conn.Open(); + var cmd = conn.CreateCommand(); + cmd.CommandText = @" + IF NOT EXISTS (SELECT * FROM sys.tables WHERE name = 'CodingSessions') + BEGIN + CREATE TABLE CodingSessions ( + Id INT PRIMARY KEY IDENTITY(1,1), + StartTime DATETIME NOT NULL, + EndTime DATETIME NOT NULL, + DurationMinutes INT NOT NULL, + Notes NVARCHAR(MAX) NULL + ) + END + ELSE IF COL_LENGTH('CodingSessions', 'DurationMinutes') IS NULL + BEGIN + ALTER TABLE CodingSessions ADD DurationMinutes INT NOT NULL DEFAULT(0); + END + + IF NOT EXISTS (SELECT * FROM sys.tables WHERE name = 'SessionLogs') + BEGIN + CREATE TABLE SessionLogs ( + Id INT PRIMARY KEY IDENTITY(1,1), + SessionId INT NULL, + Action NVARCHAR(100) NOT NULL, + Message NVARCHAR(MAX) NULL, + LoggedAt DATETIME NOT NULL DEFAULT(GETDATE()), + FOREIGN KEY (SessionId) REFERENCES CodingSessions(Id) + ) + END + ELSE + BEGIN + IF COL_LENGTH('SessionLogs', 'SessionId') IS NULL + ALTER TABLE SessionLogs ADD SessionId INT NULL; + IF COL_LENGTH('SessionLogs', 'Action') IS NULL + ALTER TABLE SessionLogs ADD Action NVARCHAR(100) NOT NULL DEFAULT(''); + IF COL_LENGTH('SessionLogs', 'Message') IS NULL + ALTER TABLE SessionLogs ADD Message NVARCHAR(MAX) NULL; + IF COL_LENGTH('SessionLogs', 'LoggedAt') IS NULL + ALTER TABLE SessionLogs ADD LoggedAt DATETIME NOT NULL DEFAULT(GETDATE()); + END"; + cmd.ExecuteNonQuery(); + } + } + + private static JsonElement LoadAppSettings() + { + var json = File.ReadAllText(APP_SETTING_PATH); + return JsonElement.Parse(json); + } } \ No newline at end of file From 4c4a0cb97fe1e54220807375cc7a1840de8018ea Mon Sep 17 00:00:00 2001 From: Kelvin Tawiah Date: Fri, 16 Jan 2026 01:02:23 -0600 Subject: [PATCH 11/13] Fix appsettings example filename typo --- .../{appsettions.example.json => appsettings.example.json} | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename KelvinTawiah.CodingTracker/{appsettions.example.json => appsettings.example.json} (100%) diff --git a/KelvinTawiah.CodingTracker/appsettions.example.json b/KelvinTawiah.CodingTracker/appsettings.example.json similarity index 100% rename from KelvinTawiah.CodingTracker/appsettions.example.json rename to KelvinTawiah.CodingTracker/appsettings.example.json From 4cc568a636cd5a92cd6e9a48d93bb37fa804653f Mon Sep 17 00:00:00 2001 From: Kelvin Tawiah Date: Fri, 16 Jan 2026 01:02:43 -0600 Subject: [PATCH 12/13] Add project README with requirements and usage instructions --- README.md | 21 +++++++++++++++++++++ 1 file changed, 21 insertions(+) create mode 100644 README.md diff --git a/README.md b/README.md new file mode 100644 index 00000000..6c9946d6 --- /dev/null +++ b/README.md @@ -0,0 +1,21 @@ +# Coding Tracker (Dapper + Spectre.Console) + +A console app to log coding sessions using Dapper for data access and Spectre.Console for the UI. + +## Requirements coverage +- **Dapper ORM** for all data access (repositories). +- **Spectre.Console** menus, prompts, and tables for display. +- **Separate classes**: `CodingController`, `UserInput`, `Validation`, services, and repositories. +- **Strict datetime format**: `yyyy-MM-dd HH:mm`; validated on input. +- **Duration computed** automatically from start/end; not user-entered. +- **Manual start/end entry** enforced; both required. +- **Configuration** via `appsettings.json` for database settings. + +## Running +1. Ensure SQL Server is accessible per `KelvinTawiah.CodingTracker/appsettings.json`. +2. From `KelvinTawiah.CodingTracker/`: `dotnet run` +3. Follow the Spectre.Console menu to add or manage sessions. + +## Notes +- Database/tables are created on first run (with duration and logging columns). +- Session logs capture create/update/delete actions with timestamps for auditability. From a7eb46035bb7b2eef4dec2385391b6f0ae521f41 Mon Sep 17 00:00:00 2001 From: Kelvin Tawiah Date: Fri, 16 Jan 2026 01:14:49 -0600 Subject: [PATCH 13/13] Refactor: Remove Requirement.md --- KelvinTawiah.CodingTracker/Requirements.md | 33 ---------------------- 1 file changed, 33 deletions(-) delete mode 100644 KelvinTawiah.CodingTracker/Requirements.md diff --git a/KelvinTawiah.CodingTracker/Requirements.md b/KelvinTawiah.CodingTracker/Requirements.md deleted file mode 100644 index 08c9dd9e..00000000 --- a/KelvinTawiah.CodingTracker/Requirements.md +++ /dev/null @@ -1,33 +0,0 @@ -Requirements - -This application has the same requirements as the previous project, except that now you'll be logging your daily coding time. - - -To show the data on the console, you should use the Spectre.Console library. - - -You're required to have separate classes in different files (i.e. UserInput.cs, Validation.cs, CodingController.cs) - - -You should tell the user the specific format you want the date and time to be logged and not allow any other format. - - -You'll need to create a configuration file called appsettings.json, which will contain your database path and connection strings (and any other configs you might need). - - -You'll need to create a CodingSession class in a separate file. It will contain the properties of your coding session: Id, StartTime, EndTime, Duration. When reading from the database, you can't use an anonymous object, you have to read your table into a List of CodingSession. - - -The user shouldn't input the duration of the session. It should be calculated based on the Start and End times - - -The user should be able to input the start and end times manually. - - -You need to use Dapper ORM for the data access instead of ADO.NET. (This requirement was included in Feb/2024) - - -Follow the DRY Principle, and avoid code repetition. - - -Don't forget the ReadMe explaining your thought process. \ No newline at end of file