From b763d704035a361e43cbc91f112b3cf60406d11b Mon Sep 17 00:00:00 2001 From: Steven Wardlaw Date: Mon, 23 Mar 2026 17:14:02 +0000 Subject: [PATCH 1/3] Initial commit - mostly finished but need to implement (end date > start date) validation --- .../codingTracker.stevenwardlaw.slnx | 3 + .../CodingController.cs | 111 ++++++++++++++++++ .../CodingSession.cs | 10 ++ .../DataValidation.cs | 23 ++++ .../codingTracker.stevenwardlaw/Program.cs | 19 +++ .../codingTracker.stevenwardlaw/UserInput.cs | 71 +++++++++++ .../appsettings.json | 5 + .../codingTracker.stevenwardlaw.csproj | 25 ++++ 8 files changed, 267 insertions(+) create mode 100644 codingTracker.stevenwardlaw/codingTracker.stevenwardlaw.slnx create mode 100644 codingTracker.stevenwardlaw/codingTracker.stevenwardlaw/CodingController.cs create mode 100644 codingTracker.stevenwardlaw/codingTracker.stevenwardlaw/CodingSession.cs create mode 100644 codingTracker.stevenwardlaw/codingTracker.stevenwardlaw/DataValidation.cs create mode 100644 codingTracker.stevenwardlaw/codingTracker.stevenwardlaw/Program.cs create mode 100644 codingTracker.stevenwardlaw/codingTracker.stevenwardlaw/UserInput.cs create mode 100644 codingTracker.stevenwardlaw/codingTracker.stevenwardlaw/appsettings.json create mode 100644 codingTracker.stevenwardlaw/codingTracker.stevenwardlaw/codingTracker.stevenwardlaw.csproj diff --git a/codingTracker.stevenwardlaw/codingTracker.stevenwardlaw.slnx b/codingTracker.stevenwardlaw/codingTracker.stevenwardlaw.slnx new file mode 100644 index 000000000..5cecfbe30 --- /dev/null +++ b/codingTracker.stevenwardlaw/codingTracker.stevenwardlaw.slnx @@ -0,0 +1,3 @@ + + + diff --git a/codingTracker.stevenwardlaw/codingTracker.stevenwardlaw/CodingController.cs b/codingTracker.stevenwardlaw/codingTracker.stevenwardlaw/CodingController.cs new file mode 100644 index 000000000..fef934705 --- /dev/null +++ b/codingTracker.stevenwardlaw/codingTracker.stevenwardlaw/CodingController.cs @@ -0,0 +1,111 @@ +using Dapper; +using Microsoft.Data.Sqlite; +using Microsoft.Extensions.Configuration; +using Spectre.Console; +using static System.Collections.Specialized.BitVector32; + + +namespace codingTracker.stevenwardlaw +{ + internal static class CodingController + { + + static string CreateConnectionString() + { + IConfiguration configuration = new ConfigurationBuilder().AddJsonFile("appsettings.json").Build(); + string connectionString = configuration.GetConnectionString("DefaultConnection")!; + + return connectionString; + } + + public static void CreateTable() + { + using (var connection = new SqliteConnection(CreateConnectionString())) + { + connection.Open(); + var tableCmd = connection.CreateCommand(); + tableCmd.CommandText = @"CREATE TABLE IF NOT EXISTS codingTracker ( + Id INTEGER PRIMARY KEY AUTOINCREMENT, + StartTime TEXT, + EndTime TEXT, + Duration INTEGER + )"; + tableCmd.ExecuteNonQuery(); + connection.Close(); + } + } + + public static int InsertRecord(string _startTime, string _endTime) + { + using (var connection = new SqliteConnection(CreateConnectionString())) + { + connection.Open(); + string sql = "INSERT INTO codingTracker (StartTime, EndTime, Duration)" + + "VALUES (@starttime, @endtime, @duration)"; + // Create object to hold the parameters and assign these from the codingSession instance + object[] parameters = { new { starttime = _startTime, endtime = _endTime, + duration = GetDuration(_startTime, _endTime)} }; + int numRecords = connection.Execute(sql, parameters); + connection.Close(); + return numRecords; + } + } + + public static int UpdateRecord(int _id, string _startTime, string _endTime) + { + using (var connection = new SqliteConnection(CreateConnectionString())) + { + connection.Open(); + string sql = "UPDATE codingTracker SET StartTime = @starttime, EndTime = @endtime," + + "Duration = @duration WHERE Id = @id"; + object[] parameters = { new { id = _id, starttime = _startTime, + endtime = _endTime, duration = GetDuration(_startTime, _endTime)} }; + int numRecords = connection.Execute(sql, parameters); + connection.Close(); + return numRecords; + } + } + + public static int DeleteRecord(int _id) + { + using (var connection = new SqliteConnection(CreateConnectionString())) + { + connection.Open(); + string sql = "DELETE FROM codingTracker WHERE Id = @id"; + int numRecords = connection.Execute(sql, new { id = _id }); + connection.Close(); + return numRecords; + } + } + + public static void GetAllRecords() + { + using (var connection = new SqliteConnection(CreateConnectionString())) + { + connection.Open(); + string sql = "SELECT * from codingTracker"; + List sessions = connection.Query(sql).ToList(); + + var table = new Table(); + table.AddColumns("Id", "Start Time", "End Time", "Duration (minutes)"); + + foreach (CodingSession session in sessions) + { + table.AddRow(session.Id.ToString(), session.StartTime, session.EndTime, session.Duration); + } + + AnsiConsole.Write(table); + connection.Close(); + } + } + + private static int GetDuration(string _startTime, string _endTime) + { + DateTime startTime = DateTime.ParseExact(_startTime, "dd-MM-yyyy HH:mm", null); + DateTime endTime = DateTime.ParseExact(_endTime, "dd-MM-yyyy HH:mm", null); + TimeSpan duration = endTime - startTime; + int durationMinutes = (int)duration.TotalMinutes; + return durationMinutes; + } + } +} diff --git a/codingTracker.stevenwardlaw/codingTracker.stevenwardlaw/CodingSession.cs b/codingTracker.stevenwardlaw/codingTracker.stevenwardlaw/CodingSession.cs new file mode 100644 index 000000000..2fd320f31 --- /dev/null +++ b/codingTracker.stevenwardlaw/codingTracker.stevenwardlaw/CodingSession.cs @@ -0,0 +1,10 @@ +namespace codingTracker.stevenwardlaw +{ + internal class CodingSession + { + public int Id { get; set; } + public string StartTime { get; set; } + public string EndTime { get; set; } + public string Duration { get; set; } + } +} diff --git a/codingTracker.stevenwardlaw/codingTracker.stevenwardlaw/DataValidation.cs b/codingTracker.stevenwardlaw/codingTracker.stevenwardlaw/DataValidation.cs new file mode 100644 index 000000000..58ea8d459 --- /dev/null +++ b/codingTracker.stevenwardlaw/codingTracker.stevenwardlaw/DataValidation.cs @@ -0,0 +1,23 @@ +namespace codingTracker.stevenwardlaw +{ + internal static class DataValidation + { + public static bool ValidateDate(string date) + { + return DateTime.TryParseExact(date, "dd-MM-yyyy HH:mm", null, 0, out DateTime result); + } + + public static bool ValidateNumber(string num) + { + return Int16.TryParse(num, out short result); + } + + public static bool IsEndDateAfter(string _startTime, string _endTime) + { + DateTime startTime = DateTime.ParseExact(_startTime, "dd-MM-yyyy HH:mm", null); + DateTime endTime = DateTime.ParseExact(_endTime, "dd-MM-yyyy HH:mm", null); + if (endTime > startTime) return true; + else return false; + } + } +} diff --git a/codingTracker.stevenwardlaw/codingTracker.stevenwardlaw/Program.cs b/codingTracker.stevenwardlaw/codingTracker.stevenwardlaw/Program.cs new file mode 100644 index 000000000..c487e2bdb --- /dev/null +++ b/codingTracker.stevenwardlaw/codingTracker.stevenwardlaw/Program.cs @@ -0,0 +1,19 @@ +namespace codingTracker.stevenwardlaw +{ + internal class Program + { + static void Main(string[] args) + { + bool appState = true; + + CodingController.CreateTable(); + + while (appState) + { + UserInput.DisplayOptions(); + } + + } + + } +} diff --git a/codingTracker.stevenwardlaw/codingTracker.stevenwardlaw/UserInput.cs b/codingTracker.stevenwardlaw/codingTracker.stevenwardlaw/UserInput.cs new file mode 100644 index 000000000..d42ff83cc --- /dev/null +++ b/codingTracker.stevenwardlaw/codingTracker.stevenwardlaw/UserInput.cs @@ -0,0 +1,71 @@ +using Spectre.Console; + +namespace codingTracker.stevenwardlaw +{ + internal static class UserInput + { + public static void DisplayOptions() + { + string option = AnsiConsole.Prompt( + new SelectionPrompt() + .Title("What would you like to do?") + .AddChoices("View all records", "Add a record", "Update a record", "Delete a record")); + + switch (option) + { + case "View all records": + AnsiConsole.Clear(); + CodingController.GetAllRecords(); + break; + case "Add a record": + AnsiConsole.Clear(); + int addedRows = CodingController.InsertRecord( + GetDateFromUser("Please enter the start date & time (in the following format dd-mm-yyy hh:mm): "), + GetDateFromUser("Please enter the end date & time (in the following format dd-mm-yyy hh:mm): ")); + if (addedRows > 0) AnsiConsole.MarkupLine("The record was [green]successfully added.[/]"); + else AnsiConsole.MarkupLine("[red]Warning:[/] No records were added."); + break; + case "Update a record": + AnsiConsole.Clear(); + CodingController.GetAllRecords(); + int updatedRows = CodingController.UpdateRecord( + GetIdFromUser("Please enter the ID of the record you want to update: "), + GetDateFromUser("Please enter the new start date and time (in the following format dd-mm-yyy hh:mm): "), + GetDateFromUser("Please enter the new end date and time (in the following format dd-mm-yyy hh:mm): ")); + if (updatedRows > 0) AnsiConsole.MarkupLine("The record was [green]successfully updated.[/]"); + else AnsiConsole.MarkupLine("[red]Warning:[/] No records were updated."); + break; + case "Delete a record": + AnsiConsole.Clear(); + CodingController.GetAllRecords(); + int deletedRows = CodingController.DeleteRecord( + GetIdFromUser("Please enter the ID of the record you want to delete: ")); + if (deletedRows > 0) AnsiConsole.MarkupLine("The record was [green]successfully deleted.[/]"); + else AnsiConsole.MarkupLine("[red]Warning:[/] No records were deleted."); + break; + } + } + + private static string GetDateFromUser(string message) + { + string date = ""; + date = AnsiConsole.Ask($"[bold]{message}[/]"); + while (!DataValidation.ValidateDate(date)) + { + date = AnsiConsole.Ask("[bold]That is not in the correct date format, please enter a correct date: [/]"); + } + + return date; + } + + private static int GetIdFromUser(string message) + { + string input = AnsiConsole.Ask($"[bold]{message}[/]"); + while (!DataValidation.ValidateNumber(input)) + { + input = AnsiConsole.Ask("[bold]That is not a valid number, please enter an ID number: [/]"); + } + return Convert.ToInt16(input); + } + } +} diff --git a/codingTracker.stevenwardlaw/codingTracker.stevenwardlaw/appsettings.json b/codingTracker.stevenwardlaw/codingTracker.stevenwardlaw/appsettings.json new file mode 100644 index 000000000..0f19094b2 --- /dev/null +++ b/codingTracker.stevenwardlaw/codingTracker.stevenwardlaw/appsettings.json @@ -0,0 +1,5 @@ +{ + "ConnectionStrings": { + "DefaultConnection": "Data Source=codingTracker.db" + } +} \ No newline at end of file diff --git a/codingTracker.stevenwardlaw/codingTracker.stevenwardlaw/codingTracker.stevenwardlaw.csproj b/codingTracker.stevenwardlaw/codingTracker.stevenwardlaw/codingTracker.stevenwardlaw.csproj new file mode 100644 index 000000000..1d7dbc2d6 --- /dev/null +++ b/codingTracker.stevenwardlaw/codingTracker.stevenwardlaw/codingTracker.stevenwardlaw.csproj @@ -0,0 +1,25 @@ + + + + Exe + net10.0 + enable + enable + + + + + + + + + + + + + + Always + + + + From a497240cf644b85026f6e1c47e7ee60fa5dde66c Mon Sep 17 00:00:00 2001 From: Steven Wardlaw Date: Tue, 24 Mar 2026 09:18:09 +0000 Subject: [PATCH 2/3] Added validation to check end time is after start time. Also added ability to allow users to filter by time periods and sort the records by start or end time (ascending or descending) --- .../CodingController.cs | 6 +- .../DataValidation.cs | 6 +- .../codingTracker.stevenwardlaw/UserInput.cs | 103 ++++++++++++++++-- 3 files changed, 101 insertions(+), 14 deletions(-) diff --git a/codingTracker.stevenwardlaw/codingTracker.stevenwardlaw/CodingController.cs b/codingTracker.stevenwardlaw/codingTracker.stevenwardlaw/CodingController.cs index fef934705..b65e98a27 100644 --- a/codingTracker.stevenwardlaw/codingTracker.stevenwardlaw/CodingController.cs +++ b/codingTracker.stevenwardlaw/codingTracker.stevenwardlaw/CodingController.cs @@ -83,7 +83,7 @@ public static void GetAllRecords() using (var connection = new SqliteConnection(CreateConnectionString())) { connection.Open(); - string sql = "SELECT * from codingTracker"; + string sql = UserInput.ViewRecordOptions(); List sessions = connection.Query(sql).ToList(); var table = new Table(); @@ -101,8 +101,8 @@ public static void GetAllRecords() private static int GetDuration(string _startTime, string _endTime) { - DateTime startTime = DateTime.ParseExact(_startTime, "dd-MM-yyyy HH:mm", null); - DateTime endTime = DateTime.ParseExact(_endTime, "dd-MM-yyyy HH:mm", null); + DateTime startTime = DateTime.ParseExact(_startTime, "yyyy-MM-dd HH:mm", null); + DateTime endTime = DateTime.ParseExact(_endTime, "yyyy-MM-dd HH:mm", null); TimeSpan duration = endTime - startTime; int durationMinutes = (int)duration.TotalMinutes; return durationMinutes; diff --git a/codingTracker.stevenwardlaw/codingTracker.stevenwardlaw/DataValidation.cs b/codingTracker.stevenwardlaw/codingTracker.stevenwardlaw/DataValidation.cs index 58ea8d459..c38f2e6b4 100644 --- a/codingTracker.stevenwardlaw/codingTracker.stevenwardlaw/DataValidation.cs +++ b/codingTracker.stevenwardlaw/codingTracker.stevenwardlaw/DataValidation.cs @@ -4,7 +4,7 @@ internal static class DataValidation { public static bool ValidateDate(string date) { - return DateTime.TryParseExact(date, "dd-MM-yyyy HH:mm", null, 0, out DateTime result); + return DateTime.TryParseExact(date, "yyyy-MM-dd HH:mm", null, 0, out DateTime result); } public static bool ValidateNumber(string num) @@ -14,8 +14,8 @@ public static bool ValidateNumber(string num) public static bool IsEndDateAfter(string _startTime, string _endTime) { - DateTime startTime = DateTime.ParseExact(_startTime, "dd-MM-yyyy HH:mm", null); - DateTime endTime = DateTime.ParseExact(_endTime, "dd-MM-yyyy HH:mm", null); + DateTime startTime = DateTime.ParseExact(_startTime, "yyyy-MM-dd HH:mm", null); + DateTime endTime = DateTime.ParseExact(_endTime, "yyyy-MM-dd HH:mm", null); if (endTime > startTime) return true; else return false; } diff --git a/codingTracker.stevenwardlaw/codingTracker.stevenwardlaw/UserInput.cs b/codingTracker.stevenwardlaw/codingTracker.stevenwardlaw/UserInput.cs index d42ff83cc..b2d5169f5 100644 --- a/codingTracker.stevenwardlaw/codingTracker.stevenwardlaw/UserInput.cs +++ b/codingTracker.stevenwardlaw/codingTracker.stevenwardlaw/UserInput.cs @@ -19,19 +19,14 @@ public static void DisplayOptions() break; case "Add a record": AnsiConsole.Clear(); - int addedRows = CodingController.InsertRecord( - GetDateFromUser("Please enter the start date & time (in the following format dd-mm-yyy hh:mm): "), - GetDateFromUser("Please enter the end date & time (in the following format dd-mm-yyy hh:mm): ")); + int addedRows = UserInsertRecord(); if (addedRows > 0) AnsiConsole.MarkupLine("The record was [green]successfully added.[/]"); else AnsiConsole.MarkupLine("[red]Warning:[/] No records were added."); break; case "Update a record": AnsiConsole.Clear(); CodingController.GetAllRecords(); - int updatedRows = CodingController.UpdateRecord( - GetIdFromUser("Please enter the ID of the record you want to update: "), - GetDateFromUser("Please enter the new start date and time (in the following format dd-mm-yyy hh:mm): "), - GetDateFromUser("Please enter the new end date and time (in the following format dd-mm-yyy hh:mm): ")); + int updatedRows = UserUpdateRecord(); if (updatedRows > 0) AnsiConsole.MarkupLine("The record was [green]successfully updated.[/]"); else AnsiConsole.MarkupLine("[red]Warning:[/] No records were updated."); break; @@ -54,7 +49,7 @@ private static string GetDateFromUser(string message) { date = AnsiConsole.Ask("[bold]That is not in the correct date format, please enter a correct date: [/]"); } - + return date; } @@ -67,5 +62,97 @@ private static int GetIdFromUser(string message) } return Convert.ToInt16(input); } + + private static int UserUpdateRecord() + { + int id = GetIdFromUser("Please enter the ID of the record you want to update: "); + string startTime = GetDateFromUser("Please enter the new start date and time (in the following format yyyy-mm-dd hh:mm e.g. 2025-12-30 16:05): "); + string endTime = GetDateFromUser("Please enter the new end date and time (in the following format yyyy-mm-dd hh:mm e.g. 2025-12-30 16:05): "); + + while (!DataValidation.IsEndDateAfter(startTime, endTime)) + { + AnsiConsole.MarkupLine("[red]Error:[/] The end time must be [bold]after[/] the start time."); + startTime = GetDateFromUser("Please enter the new start date and time (in the following format yyyy-mm-dd hh:mm e.g. 2025-12-30 16:05): "); + endTime = GetDateFromUser("Please enter the new end date and time (in the following format yyyy-mm-dd hh:mm e.g. 2025-12-30 16:05): "); + } + int num = CodingController.UpdateRecord(id, startTime, endTime); + return num; + } + + private static int UserInsertRecord() + { + string startTime = GetDateFromUser("Please enter the start date and time (in the following format yyyy-mm-dd hh:mm e.g. 2025-12-30 16:05): "); + string endTime = GetDateFromUser("Please enter the end date and time (in the following format yyyy-mm-dd hh:mm e.g. 2025-12-30 16:05): "); + + while (!DataValidation.IsEndDateAfter(startTime, endTime)) + { + AnsiConsole.MarkupLine("[red]Error:[/] The end time must be [bold]after[/] the start time."); + startTime = GetDateFromUser("Please enter the start date and time (in the following format yyyy-mm-dd hh:mm e.g. 2025-12-30 16:05): "); + endTime = GetDateFromUser("Please enter the end date and time (in the following format yyyy-mm-dd hh:mm e.g. 2025-12-30 16:05): "); + } + int num = CodingController.InsertRecord(startTime, endTime); + return num; + } + + public static string ViewRecordOptions() + { + string dateFilter; + string sortOrder; + + // Get option for filtering + string dateFilterOption = AnsiConsole.Prompt( + new SelectionPrompt() + .Title("Would you like to filter for records that started in a specific time period?") + .AddChoices("No, all records", "Past day", "Past week", "Past month", "Past year")); + + switch (dateFilterOption) + { + case "Past day": + dateFilter = "WHERE starttime > datetime('now','-1 days') "; + break; + case "Past week": + dateFilter = "WHERE starttime > datetime('now','-7 days') "; + break; + case "Past month": + dateFilter = "WHERE starttime > datetime('now','-1 month') "; + break; + case "Past year": + dateFilter = "WHERE starttime > datetime('now','-1 year') "; + break; + default: + dateFilter = ""; + break; + } + + // Get option for sorting + string sortFilterOption = AnsiConsole.Prompt( + new SelectionPrompt() + .Title("Would you like to sort the records based on the Start Time or End Time?") + .AddChoices("No", "Start Time (ascending)", "Start Time (descending)", + "End Time (ascending)", "End Time (descending)")); + + switch (sortFilterOption) + { + case "Start Time (ascending)": + sortOrder = "ORDER BY starttime ASC"; + break; + case "Start Time (descending)": + sortOrder = "ORDER BY starttime DESC"; + break; + case "End Time (ascending)": + sortOrder = "ORDER BY endtime ASC"; + break; + case "End Time (descending)": + sortOrder = "ORDER BY endtime DESC"; + break; + default: + sortOrder = ""; + break; + } + + string fullSql = "SELECT * from codingTracker " + dateFilter + sortOrder; + return fullSql; + + } } } From e8f4653108a03a0238bf8f5e4cfa93cfd01b5a8d Mon Sep 17 00:00:00 2001 From: stevenwardlaw <107995170+stevenwardlaw@users.noreply.github.com> Date: Tue, 24 Mar 2026 09:24:48 +0000 Subject: [PATCH 3/3] Initialize README with project details and requirements Added project description and requirements for tracking coding time. --- README.md | 11 +++++++++++ 1 file changed, 11 insertions(+) create mode 100644 README.md diff --git a/README.md b/README.md new file mode 100644 index 000000000..67c1536b7 --- /dev/null +++ b/README.md @@ -0,0 +1,11 @@ +Project: Tracking coding time. + +Requirements/Comments/Thoughts: +Use Dapper instead of ADO.NET for the SQLite side of things. +Use Spectre Console for displaying the information. This was really good as it allowed me to display the records in a proper table and looked much nicer. Also having the ability to allow interactive user selections is nice. It also had the added benefit of ensuring the option that was being returned to the code - removing the risk of the user typing anything they wanted into it. +Validations on dates, selections, end time must be after start time. +Use separate classes - this was good as it kept everything organised. It also made me think of where to put certain methods as some of them could have been in a couple of different classes. +Initially did the date format the same the previous one (dd-MM-yyyy HH:mm) but then changed this to match the format that SQLite reads dates (ISO 8601). I chose the date I'm familiar with from the SQL I used in work (yyyy-MM-dd HH:mm). +Created the config file - I found this a bit tricky to find out how to actually read the fields from the json file. Struggled to find information online about this but did find it eventually! +Coding session is used when reading the records from the table. +Duration is calculated by the code.