diff --git a/Coding-Tracker/.vscode/launch.json b/Coding-Tracker/.vscode/launch.json new file mode 100644 index 00000000..af41789e --- /dev/null +++ b/Coding-Tracker/.vscode/launch.json @@ -0,0 +1,20 @@ +{ + // Use IntelliSense to learn about possible attributes. + // Hover to view descriptions of existing attributes. + // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387 + "version": "0.2.0", + "configurations": [ + { + "name": "External Terminal, .NET Core Launch (console)", + "type": "coreclr", + "request": "launch", + "preLaunchTask": "${defaultBuildTask}", + "program": "C:/Users/j_men/Coding-Tracker/Coding-Tracker/bin/debug/net10.0/Coding-Tracker.dll", + "args": [], + "cwd": "${workspaceFolder}/Coding-tracker", + "stopAtEntry": false, + "console": "externalTerminal" + } + ], + +} \ No newline at end of file diff --git a/Coding-Tracker/Coding-Tracker/Coding tracker.txt b/Coding-Tracker/Coding-Tracker/Coding tracker.txt new file mode 100644 index 00000000..4d102d5c --- /dev/null +++ b/Coding-Tracker/Coding-Tracker/Coding tracker.txt @@ -0,0 +1,77 @@ +Coding tracker + +I was tasked with creating a coding tracker, an application that allows the user to Create, View/Read, Update and Delete any record that users enter. + +I needed four classes and a program file that acts as the entry point for the program. + +The coding tracker differs from the habit tracker in a few areas, the habit tracker used ADO.NET to write to the databse and to view the database we used an external terminal. + +The habit tracker also holds all the data in one class (program.cs) whereas the coding tracker uses separation of concerns to split the different functions into classes. + +An appsettings.json file was created so I could place the database connectionstring inside of it, to access the string I created a configuration builder which adds the file and builds it + +we store our new configuration as connectionstring, so whenever our database needs accessing we call on "connectionstring" which holds the data source of our database. + + +CodingController +The coding controller class is responsible for database operations this is where we make use of Dapper and Spectre. +Dapper is used to simplify the commands we need to communicate with our database. +Spectre was used in this project to present our data in a table in the terminal which makes viewing our user input neater and more presentable. + + +I created a CreateTable Method that uses the Dapper syntax to execute the SQL to create the table with the variables I chose, ID, StartTime, EndTime and duration. We use the connectionstring we created to create a variable that will communicate with the database. + +Create table is called as we need to create the table before anything happens. +I needed to create a method called viewRecords that contained my initial record viewer method GETallrecords as I faced issues with user input. +The method calls get all records then asks the user to "Press any key to return to main menu" followed by a readkey that exits the method. + +Getallrecords clears the console, uses connectionstring to create connectionString to create a connection to the SQLite database. It maps each row from the table to the coding tracker object. + +The returned collection is converted into a List and stored in the tableData variable. + +I created a new table and added columns for each variable where each variable would go. +we use a foreach loop to add rows, we convert the output tostring and write them to the table. + +The Insert, Delete and update methods in coding controller rely on variables being passed in. + +The insert method needs two variables startime and endtime as we need the two to calculate duration +The delete method needs an id to delete so we must pass in an id. +The update method needs id , startTime and datetime to overwrite the initial values. + +The variables being passed into the controller methods rely on user input. + +UserInput + +User input has methods that take the users input and pass them into our controller class methods to do calculations and to insert, delete or update records before putting them into the database. + +GeTimeInput - used to gather users input for StartTime and End Time methods. +The method is designed to write a question, collect users input and return the input after being validated. +The ValidateTime method from the Validation class is used here + +The variable we use to gather the users input is passed into ValidateTime + +This method has a string parameter that asks the user what to enter +When called upon we must write the question we are asking the user for example in InsertInput; + +startTime = GeTimeInput("Please enter the start Time Format: (HH:mm), Or enter 0 to exit."); +endTime = GeTimeInput("Please enter the end Time Format: (HH:mm), Or enter 0 to exit."); + +Then inside InsertInput when we call input " CodingController.Insert(startTime, endTime);" + +startTime and EndTime were passed into the method. + +InsertInput - Used to pass user input into the insert method + +DeleteInput - Used to pass user input into the delete method +clear the console +call the records +ask the record that we want to delete +collect it pass the input into our validation method +pass the collected input into the delete method. + + + + + + + diff --git a/Coding-Tracker/Coding-Tracker/Coding-Tracker.csproj b/Coding-Tracker/Coding-Tracker/Coding-Tracker.csproj new file mode 100644 index 00000000..b69b465e --- /dev/null +++ b/Coding-Tracker/Coding-Tracker/Coding-Tracker.csproj @@ -0,0 +1,28 @@ + + + + Exe + net10.0 + Coding_Tracker + enable + enable + + + + + + Always + + + + + + + + + + + + + + diff --git a/Coding-Tracker/Coding-Tracker/Coding-Tracker.db b/Coding-Tracker/Coding-Tracker/Coding-Tracker.db new file mode 100644 index 00000000..d5a541b1 Binary files /dev/null and b/Coding-Tracker/Coding-Tracker/Coding-Tracker.db differ diff --git a/Coding-Tracker/Coding-Tracker/CodingController.cs b/Coding-Tracker/Coding-Tracker/CodingController.cs new file mode 100644 index 00000000..cacfbf80 --- /dev/null +++ b/Coding-Tracker/Coding-Tracker/CodingController.cs @@ -0,0 +1,123 @@ + +using Microsoft.Data.Sqlite; +using Microsoft.Extensions.Configuration; +using Dapper; +using Spectre.Console; + +class CodingController +{ + // CodingController.cs — database operations + + static string connectionString = new ConfigurationBuilder() + .AddJsonFile("appsettings.json") + .Build() + .GetConnectionString("DefaultConnection") ?? throw new InvalidOperationException("Connection string not found."); + + + public static void CreateTable() + { + using (var connection = new SqliteConnection(connectionString)) + { + connection.Execute(@"CREATE TABLE IF NOT EXISTS coding_tracker ( + Id INTEGER PRIMARY KEY AUTOINCREMENT, + StartTime TEXT, + EndTime TEXT, + Duration TEXT + )"); + } + } + + public static void ViewRecords() + { + GetAllRecords(); + Console.WriteLine(); + Console.ReadKey(); + } + + public static void GetAllRecords() + { + Console.Clear(); + using (var connection = new SqliteConnection(connectionString)) + { + List tabledata = connection.Query("SELECT * FROM coding_tracker").ToList(); + + var table = new Table(); + + table.AddColumn("Id"); + table.AddColumn("Start Time"); + table.AddColumn("End Time"); + table.AddColumn("Duration"); + + foreach (var session in tabledata) + { + table.AddRow( + session.Id.ToString(), + session.StartTime.ToString(), + session.EndTime.ToString(), + session.Duration.ToString() + ); + } + + AnsiConsole.Write(table); + } + + } + + public static void Insert(DateTime startTime, DateTime endTime) + { + + string duration = (endTime - startTime).ToString(); + + using (var connection = new SqliteConnection(connectionString)) + { + int rowsAffected = connection.Execute("INSERT INTO coding_tracker (StartTime, EndTime, Duration) VALUES (@StartTime, @EndTime, @Duration)", + new { StartTime = startTime, EndTime = endTime, Duration = duration }); + Console.WriteLine("Record Inserted.\nPress any key to return to main menu."); + Console.ReadKey(); + } + } + + public static void Delete(int id) + { + + using (var connection = new SqliteConnection(connectionString)) + { + int rowsAffected = connection.Execute("DELETE FROM coding_tracker WHERE Id = @Id", new { Id = id }); + + if (rowsAffected == 0) + Console.WriteLine("No record found with that ID."); + else + Console.WriteLine("Record Deleted.\nPress any key to return to main menu."); + + Console.ReadKey(); + } + } + + public static void Update(int ID, DateTime startTime, DateTime endTime, string duration) + { + + + using (var connection = new SqliteConnection(connectionString)) + { + + int rowsAffected = connection.Execute("UPDATE coding_tracker SET StartTime = @StartTime, EndTime = @EndTime, Duration = @Duration WHERE ID = @Id", + new { Id = ID, StartTime = startTime, EndTime = endTime, Duration = duration }); + + if (rowsAffected == 0) + { + Console.WriteLine("No record found with that ID."); + } + else + Console.WriteLine("Record Updated.\nPress any key to return to main menu."); + + Console.ReadKey(); + + } + } +} + + + + + + diff --git a/Coding-Tracker/Coding-Tracker/CodingSession.cs b/Coding-Tracker/Coding-Tracker/CodingSession.cs new file mode 100644 index 00000000..33f222a1 --- /dev/null +++ b/Coding-Tracker/Coding-Tracker/CodingSession.cs @@ -0,0 +1,15 @@ +// CodingSession.cs — the model + +public class Codingtracker +{ + public int Id { get; set; } + + public DateTime StartTime { get; set; } + + public DateTime EndTime { get; set; } + + public string Duration { get; set; } + +} + + diff --git a/Coding-Tracker/Coding-Tracker/Images/Delete.png b/Coding-Tracker/Coding-Tracker/Images/Delete.png new file mode 100644 index 00000000..8cd56ffa Binary files /dev/null and b/Coding-Tracker/Coding-Tracker/Images/Delete.png differ diff --git a/Coding-Tracker/Coding-Tracker/Images/Insert.png b/Coding-Tracker/Coding-Tracker/Images/Insert.png new file mode 100644 index 00000000..eba61bb9 Binary files /dev/null and b/Coding-Tracker/Coding-Tracker/Images/Insert.png differ diff --git a/Coding-Tracker/Coding-Tracker/Images/Menu.png b/Coding-Tracker/Coding-Tracker/Images/Menu.png new file mode 100644 index 00000000..b9a99024 Binary files /dev/null and b/Coding-Tracker/Coding-Tracker/Images/Menu.png differ diff --git a/Coding-Tracker/Coding-Tracker/Images/Update.png b/Coding-Tracker/Coding-Tracker/Images/Update.png new file mode 100644 index 00000000..a3dfca83 Binary files /dev/null and b/Coding-Tracker/Coding-Tracker/Images/Update.png differ diff --git a/Coding-Tracker/Coding-Tracker/Images/ViewSpectre.png b/Coding-Tracker/Coding-Tracker/Images/ViewSpectre.png new file mode 100644 index 00000000..6b85260c Binary files /dev/null and b/Coding-Tracker/Coding-Tracker/Images/ViewSpectre.png differ diff --git a/Coding-Tracker/Coding-Tracker/Program1.cs b/Coding-Tracker/Coding-Tracker/Program1.cs new file mode 100644 index 00000000..d4f3f92e --- /dev/null +++ b/Coding-Tracker/Coding-Tracker/Program1.cs @@ -0,0 +1,18 @@ +using Microsoft.Data.Sqlite; +using Dapper; +using Spectre.Console; + +class Program1 +{ + static string connectionString = @"Data Source=habit-Tracker.db"; + + static void Main(string[] args) + { + { + CodingController.CreateTable(); + Input.GetUserInput(); + Console.ReadLine(); + } + } + +} diff --git a/Coding-Tracker/Coding-Tracker/UserInput.cs b/Coding-Tracker/Coding-Tracker/UserInput.cs new file mode 100644 index 00000000..f5682e15 --- /dev/null +++ b/Coding-Tracker/Coding-Tracker/UserInput.cs @@ -0,0 +1,115 @@ +using System.Runtime.ConstrainedExecution; + +public class Input +{ + public static void GetUserInput() + { + DateTime startTime; + DateTime endTime; + + bool end = false; + while (end == false) + { + Console.Clear(); + Console.WriteLine("Welcome to the coding tracker!\n"); + Console.WriteLine("What would you like to do?"); + Console.WriteLine("Press 0 to close the console."); + Console.WriteLine("Press 1 to view Records."); + Console.WriteLine("Press 2 to Insert a Record."); + Console.WriteLine("Press 3 to Delete a Record."); + Console.WriteLine("Press 4 to Update a Record."); + + string command = Console.ReadLine(); + + switch (command) + { + case "0": + end = true; + Console.WriteLine("Goodbye\nPress any key to quit."); + Console.ReadLine(); + Environment.Exit(0); + break; + + case "1": + CodingController.ViewRecords(); + break; + + case "2": + InsertInput(); + break; + + case "3": + DeleteInput(); + break; + + case "4": + UpdateInput(); + break; + + default: + Console.WriteLine("Please enter an option between 1 and 4."); + break; + } + + } + } + + public static DateTime? GeTimeInput(string message) + { + Console.WriteLine(message); + string getTime = Console.ReadLine(); + + return Validation.ValidateTime(getTime); + + } + + public static void InsertInput() + { + Console.Clear(); + + DateTime? startTime = GeTimeInput("Please enter the start Time Format: (dd-MM-yyyy, HH:mm), Or enter 0 to exit."); + if (startTime == null) return; + + DateTime? endTime = GeTimeInput("Please enter the end Time Format: (dd-MM-yyyy, HH:mm), Or enter 0 to exit."); + if (endTime == null) return; + + + CodingController.Insert(startTime.Value, endTime.Value); + } + + public static void DeleteInput() + { + Console.Clear(); + CodingController.GetAllRecords(); + + Console.WriteLine("Please enter a record ID to delete.\n\nOr enter 0 to exit"); + + + int id = Validation.ValidateId(Console.ReadLine()); + if (id == 0) return; + + CodingController.Delete(id); + } + + public static void UpdateInput() + { + Console.Clear(); + CodingController.GetAllRecords(); + + Console.WriteLine("Please type the Id of the record you want to update, or enter 0 to return to the main menu."); + int ID = Validation.ValidateId(Console.ReadLine()); + if (ID == 0) return; + + Console.WriteLine("Please enter the new start time (dd-MM-yyyy, HH:mm), or enter 0 to return to the main menu."); + DateTime? startTime = Validation.ValidateTime(Console.ReadLine()); + if (startTime == null) return; + + Console.WriteLine("Please enter the new end time (dd-MM-yyyy, HH:mm), or enter 0 to return to the main menu."); + DateTime? endTime = Validation.ValidateEndTime(startTime.Value); + if (endTime == null) return; + + string duration = (endTime.Value - startTime.Value).ToString(); + + CodingController.Update(ID, startTime.Value, endTime.Value, duration); + } +} \ No newline at end of file diff --git a/Coding-Tracker/Coding-Tracker/Validation.cs b/Coding-Tracker/Coding-Tracker/Validation.cs new file mode 100644 index 00000000..850ca425 --- /dev/null +++ b/Coding-Tracker/Coding-Tracker/Validation.cs @@ -0,0 +1,48 @@ +using System.Globalization; + +class Validation +{ + public static DateTime? ValidateTime(string getTime) + { + DateTime result; + + while (!DateTime.TryParseExact(getTime, "dd-MM-yyyy HH:mm", null, DateTimeStyles.None, out result)) + { + if (getTime == "0") return null; + + Console.WriteLine("Invalid format. Try again (dd-MM-yyyy, HH:mm): \nOr enter 0 to exit. "); + getTime = Console.ReadLine(); + } + + return result; + + } + + public static DateTime? ValidateEndTime(DateTime startTime) + { + DateTime? endTime = ValidateTime(Console.ReadLine()); + + while (endTime != null && endTime <= startTime) + { + Console.WriteLine("End time must be after start time. Try again (dd-MM-yyyy, HH:mm), or enter 0 to return to the main menu: "); + endTime = ValidateTime(Console.ReadLine()); + } + + return endTime; + } + public static int ValidateId(string Input) + { + int result; + + while (!int.TryParse(Input, out result) || (result <= 0)) + { + + if (Input == "0") return 0; + + Console.WriteLine("Please enter a valid id, or press 0 to return to the main menu"); + Input = Console.ReadLine(); + } + + return result; + } +} \ No newline at end of file diff --git a/Coding-Tracker/Coding-Tracker/appsettings.json b/Coding-Tracker/Coding-Tracker/appsettings.json new file mode 100644 index 00000000..9fdb8f5f --- /dev/null +++ b/Coding-Tracker/Coding-Tracker/appsettings.json @@ -0,0 +1,5 @@ +{ + "ConnectionStrings": { + "DefaultConnection": "Data Source=Coding-Tracker.db" + } +} \ No newline at end of file diff --git a/README.md b/README.md new file mode 100644 index 00000000..2b9a7963 --- /dev/null +++ b/README.md @@ -0,0 +1,319 @@ +# Coding Tracker + + +The coding tracker is a C# console application I'm building to log and manage the duration of code sessions. + +I built this as part of the C# Academy curriculum. + + +## What it does: + +Log coding sessions with a start time, end time, and auto calculated duration including date + +View all sessions in a formatted table + +Insert, update, and delete and view created sessions + +Input validation enforcing a strict date/time format + + +## How to run + +### Prerequisites + +- [.NET 10 SDK](https://dotnet.microsoft.com/download) + +### Steps + +```bash +git clone https://github.com/jcisco22/CodeReviews.Console.CodingTracker.git +cd CodeReviews.Console.CodingTracker/Coding-Tracker/Coding-Tracker +dotnet run +``` + +The database file is created automatically on first run. + + +## Tech stack: + +C# — .NET 10 console app + +SQLite — local file based database, packaged as Microsoft.Data.Sqlite + +Dapper — Object relational mapper used for mapping query results to C# objects + +Spectre.Console — Used to create visually appealing tables in the formatted table output in the terminal + + +## How it works: + +When running the program the user will be greeted with a menu. + +The menu presents the user with five options shown in the image below with the first option being to close the program. + +The rest of the options are Create Read Update and Delete operations labelled from 1 - 4. + + +![Main Menu](Coding-Tracker/Coding-Tracker/Images/Menu.png) + + +The First CRUD operation is to view the coding tracker records: + +The table consists of the four properties Id, Start Time, End Time and Duration. + +![Viewing a record](Coding-Tracker/Coding-Tracker/Images/ViewSpectre.png) + + + +The second CRUD operation is to insert a record: + +The insert menu asks a user to enter a start and end time in the format provided including the date. + +If successfully inserted, a message showing the record was inserted is shown, followed by a return to the main menu option. + +![Inserting a record](Coding-Tracker/Coding-Tracker/Images/Insert.png) + + + +The third CRUD operation is to delete a record: + +The program shows a list of the records in the database and prompts the user to enter the id number of the record to delete. + +If successfully deleted, a message showing the record was deleted is shown, followed by a return to the main menu option. + +![Deleting a record](Coding-Tracker/Coding-Tracker/Images/Delete.png) + + +The fourth and final operation is the update record feature: + +The program prompts the user to enter the id of the record they'd like updated then asks the user to enter a new start and end time. + +If successfully updated, a message showing the record was updated is shown, followed by a return to the main menu option. + +![Updating a record](Coding-Tracker/Coding-Tracker/Images/Update.png) + + + + +## Project structure: + + +Program.cs is the entry point, it creates the table and starts the menu by calling the CreateTable method which lives in CodingController and the GetUserInput method which lives in the UserInput class. + +CodingSession.cs is the model, it holds the properties used throughout the program, it mirrors the database table. + +CodingController.cs is for database operations, all CRUD methods live inside this class, Dapper is used to execute the raw SQLite queries. + +UserInput.cs is for user interaction, this class contains the switch menu, input collection methods and contains methods that call the CodingController class. + +Validation.cs is for input validation, this class holds the ValidateTime, ValidateId and ValidateEndTime methods used for date format and ID validation. + +appsettings.json is for configuration, this file stores the database connection string. + + +## Separation of concerns + +Each class has one job. + +This is the first time I used classes in a program, the project requirements made separation of concerns a must for this project. + +I decided to split the program into four main classes: + + +UserInput talks to the user, displays menus, collects input, calls controller methods. + +CodingController talks to the database, receives parameters, runs SQL. + +Validation checks input. When input is invalid, it shows an error message and asks the user to try again. + +CodingSession is the model, containing the properties used throughout the program. + + +## Database Layout: + + +The table is created on startup if it does not already exist: + + +```` +CREATE TABLE IF NOT EXISTS coding_tracker ( +Id INTEGER PRIMARY KEY AUTOINCREMENT, +StartTime TEXT, +EndTime TEXT, +Duration TEXT +) + +```` + +Id is of type INTEGER, this is the primary key of the data, it is set to auto-increment. + + + +StartTime is of type TEXT, the format of the start time is dd-MM-yyyy HH:mm. + + + +EndTime is of type TEXT, it follows the same format of StartTime which is dd-MM-yyyy HH:mm. + + + +Duration is calculated as EndTime - StartTime, which gives a TimeSpan. SQLite has no built in DateTime or TimeSpan types, so the duration is converted to a string and stored as TEXT. + + + +Dates are stored as strings and parsed in C#. + + + +## Duration calculation: + +Duration is not entered by the user. Subtracting one DateTime from another gives a TimeSpan which is converted to a string before being stored in SQLite: + +``` +string duration = (endTime - startTime).ToString(); +``` + +## Validation: + +Date input is validated with DateTime.TryParseExact, enforcing the format "dd-MM-yyyy HH:mm". + +The ValidateTime method ensures that the user enters the correct format by looping until the correct format is received: + + +``` + public static DateTime ValidateTime(string getTime) + { + DateTime result; + + while (!DateTime.TryParseExact(getTime, "dd-MM-yyyy HH:mm", null, DateTimeStyles.None, out result)) + { + Console.WriteLine("Invalid format. Try again (dd-MM-yyyy HH:mm): "); + getTime = Console.ReadLine(); + } + + return result; + } +``` + +ID input is validated with int.TryParse, rejecting non numeric input and values less than or equal to zero. + +## End time validation: + +ValidateEndTime was added after code review pointed out that an end time could be earlier than the start time, producing a negative duration. It takes the already-validated start time and uses a while loop to keep asking for an end time until it is later than the start, preventing zero or negative durations. + + +``` + public static DateTime ValidateEndTime(DateTime startTime) + { + DateTime endTime = ValidateTime(Console.ReadLine()); + + while (endTime <= startTime) + { + Console.WriteLine("End time must be after start time.\nPlease try again (dd-MM-yyyy HH:mm): "); + endTime = ValidateTime(Console.ReadLine()); + } + + return endTime; + } + +``` + + + +## Rows affected: + +connection.Execute returns the number of rows affected. Used to detect when a user enters an ID that does not exist: + + + +``` +int rowsAffected = connection.Execute(sql, parameters); +if (rowsAffected == 0) + Console.WriteLine("No record found with that ID."); +``` + + + +## Key concepts: + +### Dapper: + +Dapper sits on top of ADO.NET and removes the need to manually map database columns to the properties. + +connection.Query(sql) — runs a SELECT and maps each row to a CodingSession object. + +connection.Execute(sql, parameters) — runs INSERT, UPDATE, DELETE operations. + +Parameters are passed as anonymous objects and being assigned value: new { Id = id, StartTime = startTime }. + +The property names on the anonymous object must match the @placeholder names in the SQL exactly or values won't be passed over and values will be null. + + +### Connection string: + +The database connection string is stored in appsettings.json rather than being hardcoded. This keeps configuration separate from code, so the database location can be changed without editing or recompiling the program. + + +``` +{ + "ConnectionStrings": { + "DefaultConnection": "Data Source=Coding-Tracker.db" + } +} +``` + +### Reading the connection string + +The connection string needed to access the database is read into a static field using ConfigurationBuilder: + + +``` +static string connectionString = new ConfigurationBuilder() + .AddJsonFile("appsettings.json") + .Build() + .GetConnectionString("DefaultConnection") + ?? throw new InvalidOperationException("Connection string not found."); +``` + +If the connection string is missing, GetConnectionString returns null. The ?? operator then throws an exception with a not found message so the program fails immediately instead of hitting a confusing null error later. + + +### Copying appsettings.json to the build output + +``` + + + Always + + +``` +This configuration in the .csproj file ensures that appsettings.json is copied to the build output directory whenever the project is built. This is necessary because the application needs to access the file at runtime to retrieve the database connection string. Without the file in the output directory, the application may be unable to load its configuration. + + + +## Reflection: + +When initially submitting my project for review, my project could not be approved due to an error with the Duration field. I had correctly calculated and stored the duration when a record was being inserted but the update method would only update the StartTime and EndTime fields leaving out duration which is the thing we are actually tracking. This meant the record could contain an outdated duration after an update. To fix this issue I added duration as a parameter to the update method and included it in the SQL UPDATE statement. This ensures that StartTime, EndTime and Duration are all updated together when a record is modified. + + +I prematurely committed the program before verifying the issue had been resolved, after running the program I realised that it hadn't. After updating a record and viewing it I noticed that the duration column was still missing as I hadn't updated the UpdateInput method in the UserInput class to assign the duration field the result of EndTime minus StartTime which I did with the line string duration = (endTime - startTime).ToString(); converting it to string as duration is stored as string. After adding this line I ran the program without this issue. + +The second issue returned from my code review was end time could be earlier than start time which is a clear problem as it would be calculated as a negative duration. To solve this issue I created a new validation method called ValidateEndTime. The method starts by creating a local variable named endTime and assigning it the value of the ValidateTime method which checks that the user’s input is in the correct format. ValidateEndTime receives the start time as a DateTime parameter, which is used to compare against the end time. A while loop checks whether the end time is earlier than or equal to the start time; if it is, the user is shown an error message and asked to enter the end time again. The loop repeats until a valid end time is entered, and the method then returns it. The insert menu applies the same check inside InsertInput, so the user can still enter 0 to exit. + +Overall I think the project taught me a lot, mainly about separation of concerns as well as validation and what ORMs are by using Dapper. I gained a better understanding of how separating input logic from database operations makes code easier to maintain, and how validation helps prevent invalid records from being stored and even accepted. I also learned how Dapper simplifies database interactions by mapping query results to C# objects and allowing SQL queries to be executed directly from the application. + +Refactoring for the time propmts was a little different as the ValidateTime and ValidateEndTime methods return types were DateTime, returning 0 wasnt an option here. I needed a value that could mean nothing, so I changed ValidateTime and ValidateEndTime to return a nullable DateTime?. If the user enters 0, these methods return null, and InsertInput and UpdateInput return to the main menu when they receive it. While making this change, I also removed a call to GetUserInput from GeTimeInput, which had been opening a new menu on top of the existing one. Changing the DateTime methods to DateTime? in the validation class meant i had to do it throughout the program.Lastly I updated InsertInput to use ValidateEndTime instead of its own copy of the end time check. + + +## Limitations : + +As duration is stored as text, calculations like "total hours coded this week" can’t be calculated in SQLite. + +Some CodingController methods write directly to the console. The messages are needed, but displaying them is UserInput’s job, so this breaks separation of concerns. A cleaner design would return a result and let UserInput show the message. + + + + + + +