diff --git a/Dania.CodingTracker/Assets/1.png b/Dania.CodingTracker/Assets/1.png new file mode 100644 index 000000000..1865b4797 Binary files /dev/null and b/Dania.CodingTracker/Assets/1.png differ diff --git a/Dania.CodingTracker/Assets/2.png b/Dania.CodingTracker/Assets/2.png new file mode 100644 index 000000000..f1e405486 Binary files /dev/null and b/Dania.CodingTracker/Assets/2.png differ diff --git a/Dania.CodingTracker/Assets/3.png b/Dania.CodingTracker/Assets/3.png new file mode 100644 index 000000000..32d383ed5 Binary files /dev/null and b/Dania.CodingTracker/Assets/3.png differ diff --git a/Dania.CodingTracker/CodingTracker/CodingTracker.csproj b/Dania.CodingTracker/CodingTracker/CodingTracker.csproj new file mode 100644 index 000000000..f559ad53d --- /dev/null +++ b/Dania.CodingTracker/CodingTracker/CodingTracker.csproj @@ -0,0 +1,23 @@ + + + + Exe + net10.0 + enable + enable + + + + + + + + + + + + PreserveNewest + + + + diff --git a/Dania.CodingTracker/CodingTracker/CodingTracker.slnx b/Dania.CodingTracker/CodingTracker/CodingTracker.slnx new file mode 100644 index 000000000..ba103e54f --- /dev/null +++ b/Dania.CodingTracker/CodingTracker/CodingTracker.slnx @@ -0,0 +1,3 @@ + + + diff --git a/Dania.CodingTracker/CodingTracker/DatabaseManager.cs b/Dania.CodingTracker/CodingTracker/DatabaseManager.cs new file mode 100644 index 000000000..016570114 --- /dev/null +++ b/Dania.CodingTracker/CodingTracker/DatabaseManager.cs @@ -0,0 +1,105 @@ +using CodingTracker.Models; +using Dapper; +using Microsoft.Data.Sqlite; +using Microsoft.Extensions.Configuration; + +namespace CodingTracker +{ + internal class DatabaseManager + { + //Appsetting.json config connection + public string GetConnectionString() + { + IConfiguration config = new ConfigurationBuilder().AddJsonFile("appsettings.json").Build(); + string connectionString = config.GetConnectionString("DefaultConnection"); + + return connectionString; + + } + + internal void CreateTable() + { + using (var connection = new SqliteConnection(GetConnectionString())) + { + var sql = @"CREATE TABLE IF NOT EXISTS coding_tracker( + Id INTEGER PRIMARY KEY AUTOINCREMENT, + StartTime TEXT, + EndTime TEXT, + Duration TEXT)"; + + connection.Execute(sql); //Dapper execute + } + } + + // Return rows affected checks if the execute succeeds or not + internal int Post(CodingSession session) + { + using (var connection = new SqliteConnection(GetConnectionString())) + { + var sql = "INSERT INTO coding_tracker (StartTime, EndTime, Duration) VALUES (@StartTime, @EndTime, @Duration)"; + + var rowsAffected = connection.Execute(sql,session); //Dapper Execute + return rowsAffected; + } + } + + internal List Get() + { + using (var connection = new SqliteConnection(GetConnectionString())) + { + var sql = "SELECT * FROM coding_tracker"; + + var sessions = connection.Query(sql); + + return sessions.ToList(); + } + } + + + internal void Delete(int id) + { + using (var connection = new SqliteConnection(GetConnectionString())) + { + var sql = "DELETE FROM coding_tracker WHERE Id = @Id"; + + connection.Execute(sql, new {Id = id }); + } + } + + internal void Update(CodingSession session) + { + using (var connection = new SqliteConnection(GetConnectionString())) + { + var sql = @"UPDATE coding_tracker + SET StartTime = @StartTime, + EndTime = @EndTime, + Duration = @Duration + WHERE Id = @Id"; + + connection.Execute(sql, session); + } + } + + internal bool CheckIdExists(int id) + { + using (var connection = new SqliteConnection(GetConnectionString())) + { + var sql = "SELECT COUNT(*) FROM coding_tracker WHERE Id = @Id"; + + int count = connection.ExecuteScalar(sql, new { Id = id}); + + return count > 0; + } + } + + internal CodingSession GetSessionByID(int id) + { + using (var connection = new SqliteConnection(GetConnectionString())) + { + var sql = "SELECT * FROM coding_tracker where Id = @Id"; + return connection.QuerySingle(sql, new { Id = id }); + } + + } + } +} diff --git a/Dania.CodingTracker/CodingTracker/Enums.cs b/Dania.CodingTracker/CodingTracker/Enums.cs new file mode 100644 index 000000000..8f51c77c2 --- /dev/null +++ b/Dania.CodingTracker/CodingTracker/Enums.cs @@ -0,0 +1,14 @@ +namespace CodingTracker +{ + internal class Enums + { + internal enum MenuOptions + { + ViewSessions, + InsertSessions, + UpdateSessions, + DeleteSessions, + CloseApp + } + } +} diff --git a/Dania.CodingTracker/CodingTracker/Helpers.cs b/Dania.CodingTracker/CodingTracker/Helpers.cs new file mode 100644 index 000000000..36a4b3bb9 --- /dev/null +++ b/Dania.CodingTracker/CodingTracker/Helpers.cs @@ -0,0 +1,50 @@ +using Spectre.Console; +using System.Globalization; + +namespace CodingTracker +{ + internal class Helpers + { + CultureInfo enUS = new CultureInfo("en-US"); + + internal string CheckDateTime() + { + string date = ""; + bool isValid = false; + + do + { + date = AnsiConsole.Ask("Format is [green]dd-MM-yy HH:mm[/] or type t for today's date and time: "); + date = date.Trim().ToLower(); + + if (date == "t") + date = DateTime.Now.ToString("dd-MM-yy HH:mm"); + + if (date != "0") + isValid = DateTime.TryParseExact(date, "dd-MM-yy HH:mm", enUS, DateTimeStyles.None, out _); + else + isValid = true; + + if (!isValid) + { + AnsiConsole.Markup("[red]Please input the right date format![/]\n"); + Console.ReadLine(); + } + + } while (!isValid); + + return date; + } + + internal TimeSpan GetDuration(string startDateTime, string endDateTime) + { + DateTime start = DateTime.ParseExact(startDateTime, "dd-MM-yy HH:mm", null); + DateTime end = DateTime.ParseExact(endDateTime, "dd-MM-yy HH:mm", null); + + TimeSpan duration = end - start; + + return duration; + } + + } +} diff --git a/Dania.CodingTracker/CodingTracker/Models/CodingSession.cs b/Dania.CodingTracker/CodingTracker/Models/CodingSession.cs new file mode 100644 index 000000000..da4252518 --- /dev/null +++ b/Dania.CodingTracker/CodingTracker/Models/CodingSession.cs @@ -0,0 +1,10 @@ +namespace CodingTracker.Models +{ + 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/Dania.CodingTracker/CodingTracker/Program.cs b/Dania.CodingTracker/CodingTracker/Program.cs new file mode 100644 index 000000000..0118c77f3 --- /dev/null +++ b/Dania.CodingTracker/CodingTracker/Program.cs @@ -0,0 +1,14 @@ +namespace CodingTracker +{ + internal class Program + { + static void Main(string[] args) + { + DatabaseManager databaseManager = new DatabaseManager(); + UserInterface userInterface = new UserInterface(); + + databaseManager.CreateTable(); + userInterface.MainMenu(); + } + } +} diff --git a/Dania.CodingTracker/CodingTracker/UserInterface.cs b/Dania.CodingTracker/CodingTracker/UserInterface.cs new file mode 100644 index 000000000..77c65c56c --- /dev/null +++ b/Dania.CodingTracker/CodingTracker/UserInterface.cs @@ -0,0 +1,249 @@ +using Spectre.Console; +using CodingTracker.Models; + +namespace CodingTracker +{ + internal class UserInterface + { + DatabaseManager databaseManager = new DatabaseManager(); + Helpers helpers = new Helpers(); + + internal void MainMenu() + { + bool isCloseApp = false; + while (!isCloseApp) + { + AnsiConsole.Clear(); + AnsiConsole.MarkupLine("[bold yellow]---Welcome to the Coding Tracker[/]---\n"); + + var choice = AnsiConsole.Prompt( + new SelectionPrompt() + .Title("What would you like to do?") + .AddChoices(Enum.GetValues())); + + switch (choice) + { + case Enums.MenuOptions.ViewSessions: + ViewSessions(); + break; + case Enums.MenuOptions.InsertSessions: + InsertSessions(); + break; + case Enums.MenuOptions.UpdateSessions: + UpdateSessions(); + break; + case Enums.MenuOptions.DeleteSessions: + DeleteSessions(); + break; + case Enums.MenuOptions.CloseApp: + isCloseApp = true; + break; + + } + + } + } + + private void ViewSessions() + { + var sessions = databaseManager.Get(); + + if (sessions.Count == 0) + { + AnsiConsole.MarkupLine("[red]No sessions recorded.[/]\nPress enter key to go back to Main Menu."); + Console.ReadLine(); + return; + } + + DrawTable(sessions); + + AnsiConsole.MarkupLine("Press enter key to go back to Main Menu."); + Console.ReadLine(); + } + + private void InsertSessions() + { + AnsiConsole.Clear(); + AnsiConsole.MarkupLine("[bold yellow]---Insert Coding Sessions---[/]"); + + AnsiConsole.MarkupLine("Please insert the [bold green]start[/] date and time (or type 0 to go back to the Main Menu)."); + var startDateTime = helpers.CheckDateTime(); + if (startDateTime == "0") return; + + AnsiConsole.MarkupLine("\nPlease insert the [bold green]end[/] date and time (or type 0 to go back to the Main Menu)."); + var endDateTime = helpers.CheckDateTime(); + if (endDateTime == "0") return; + + var duration = helpers.GetDuration(startDateTime, endDateTime); + + if (duration < TimeSpan.Zero) + { + AnsiConsole.Markup("[red]End date cannot be before start date! Please try again.[/]\n"); + Console.ReadLine(); + + } + else + { + CodingSession session = new CodingSession(); + session.StartTime = startDateTime; + session.EndTime = endDateTime; + session.Duration = $"{(int)duration.TotalHours:D2}:{duration.Minutes:D2}"; + + int rows = databaseManager.Post(session); + + if (rows > 0) + { + AnsiConsole.MarkupLine("\n[green]Session added sucessfully![/]"); + Console.ReadLine(); + } + else + { + AnsiConsole.MarkupLine("\n[red]Session added failed! Try again later[/]"); + Console.ReadLine(); + } + + } + + } + + private void UpdateSessions() + { + AnsiConsole.Clear(); + AnsiConsole.MarkupLine("[bold yellow]---Update Coding Sessions---[/]"); + + var sessions = databaseManager.Get(); + + if (sessions.Count == 0) + { + AnsiConsole.MarkupLine("\n[red]No sessions recorded to update.[/]\nPress enter key to go back to Main Menu."); + Console.ReadLine(); + return; + } + + DrawTable(sessions); + + int inputID = AnsiConsole.Ask("Please insert the number ID to update the session (or type 0 to go back to the Main Menu). "); + + if (inputID == 0) return; + + if (!databaseManager.CheckIdExists(inputID)) + { + AnsiConsole.MarkupLine($"\n[red]Session with ID {inputID} doesn't exist.[/]"); + Console.ReadLine(); + UpdateSessions(); + } + else + { + var sessionChoice = databaseManager.GetSessionByID(inputID); + + bool isUpdating = true; + while (isUpdating) + { + AnsiConsole.Clear(); + AnsiConsole.MarkupLine("[bold yellow]---Update Coding Sessions---[/]"); + DrawTable(sessions); + + var choice = AnsiConsole.Prompt(new SelectionPrompt() + .Title($"What would you like to do with Session {inputID}?") + .AddChoices("Update Start Time","Update End Time","Save and Update Database","Return to Main Menu")); + + switch (choice) + { + case "Update Start Time": + AnsiConsole.MarkupLine("Please insert the [bold green]start[/] date and time."); + sessionChoice.StartTime = helpers.CheckDateTime(); + break; + case "Update End Time": + AnsiConsole.MarkupLine("\nPlease insert the [bold green]end[/] date and time."); + sessionChoice.EndTime = helpers.CheckDateTime(); + break; + case "Save and Update Database": + var duration = helpers.GetDuration(sessionChoice.StartTime, sessionChoice.EndTime); + if (duration < TimeSpan.Zero) + { + AnsiConsole.Markup("[red]End date cannot be before start date! Please try again.[/]\n"); + Console.ReadLine(); + } + else + { + sessionChoice.Duration = $"{(int)duration.TotalHours:D2}:{duration.Minutes:D2}"; + databaseManager.Update(sessionChoice); + AnsiConsole.Markup("[green]Session has been updated![/]\n"); + Console.ReadLine(); + isUpdating = false; + } + break; + case "Return to Main Menu": + isUpdating = false; + break; + + } + } + + } + } + + + private void DeleteSessions() + { + AnsiConsole.Clear(); + AnsiConsole.MarkupLine("[bold yellow]---Delete Coding Sessions---[/]"); + + var sessions = databaseManager.Get(); + + if (sessions.Count == 0) + { + AnsiConsole.MarkupLine("\n[red]No sessions recorded to delete.[/]\nPress enter key to go back to Main Menu."); + Console.ReadLine(); + return; + } + + DrawTable(sessions); + + int inputID = AnsiConsole.Ask("Please insert the number ID to delete the session (or type 0 to go back to the Main Menu). "); + + if (inputID == 0) return; + + if (!databaseManager.CheckIdExists(inputID)) + { + AnsiConsole.MarkupLine($"\n[red]Session with ID {inputID} doesn't exist.[/]"); + Console.ReadLine(); + DeleteSessions(); + } + else + { + if (AnsiConsole.Confirm("Are you sure you want to delete this session?")) + { + databaseManager.Delete(inputID); + AnsiConsole.MarkupLine($"\n[green]Session with ID {inputID} has been deleted![/]"); + Console.ReadLine(); + } + else + { + AnsiConsole.MarkupLine("[red]Session deletion cancelled[/]"); + Console.ReadLine(); + return; + } + } + } + + + private void DrawTable(List sessions) + { + var table = new Table(); + + table.AddColumn("[bold]ID[/]"); + table.AddColumn("[bold]Start Date[/]"); + table.AddColumn("[bold]End Date[/]"); + table.AddColumn("[bold]Duration[/]"); + + foreach (var session in sessions) + { + table.AddRow(session.Id.ToString(), $"{session.StartTime}", $"{session.EndTime}", $"{session.Duration}"); + } + + AnsiConsole.Write(table); + } + + } +} diff --git a/Dania.CodingTracker/CodingTracker/appsettings.json b/Dania.CodingTracker/CodingTracker/appsettings.json new file mode 100644 index 000000000..0908081bd --- /dev/null +++ b/Dania.CodingTracker/CodingTracker/appsettings.json @@ -0,0 +1,5 @@ +{ + "ConnectionStrings": { + "DefaultConnection": "Data Source=coding-tracker.db" + } +} \ No newline at end of file diff --git a/Dania.CodingTracker/README.md b/Dania.CodingTracker/README.md new file mode 100644 index 000000000..0789a6ab7 --- /dev/null +++ b/Dania.CodingTracker/README.md @@ -0,0 +1,32 @@ +# Coding Tracker +Coding Tracker is the third green belt project from the C# Academy. It is similar to the Habit Logger on having CRUD functions but the difference is the app asks for the user's beginning coding date time and end coding date time to find the coding time duration. This is also an opportunity to learn Object-Oriented-Programming (OOP). I programmed using C#, SQlite, Dapper and Spectre.Console with Visual Studio 2026. + +## Requirements +- Same requirements as the habit logger in terms of database and CRUD. +- Have separate classes in different files. +- Create a configuration file called appsettings.json which contains the database path and connection strings. +- User should put their start time and end time in specific format and not their duration. Duration should be calculated based on the start time and end time. +- Data need to be shown through Spectre.Console library +- Need to use Dapper ORM for data access instead of ADO.NET. + +## Features +- Console based UI using the Spectre.Console library +![Image](/Assets/1.png) +- CRUD functions: + - Users insert their start date time and end date time in format dd/MM/yy HH:mm + - Users can read, update and delete their coding sessions by inputting the session id. + - Dates and numbers are validated to check if they're in the right format and check if the end date submission has to be after the start date. +![Image](/Assets/2.png) + +## Challenges +- Understanding what appsettings.json. This is the first hurdle of trying to understand what this script does. From my understanding it's a configuration file that stores data like the database connection string which is more secure than hardcoding it in the C# script. This was hardcoded in the habit log project. +- Learning Dapper ORM. I thought it would be another long code I had to learn, but it actually condensed the code from giving large command text to small sql strings to use Dapper's execute and Query(). It also teaches me to use @Parameters from SQL injection attacks and anonymous object to bridge the SQL parameters with the local paremeters. +- Spectre.Console is not that challenging to learn as they provide a very clear documentation on how to use them. The hard part is how to use them to separate the data and visuals in different classes from each other. +- Calculating the duration between two date times. I had to use 24 hour time to calculate the duration as it doesn't require to convert for calculations. The calculations is just simply subtract the end date with the start date with TimeSpan as a data type to represent total hours and minutes of the duration. + +## References +- https://www.nuget.org/packages/Microsoft.Extensions.Configuration.Json +- https://stackoverflow.com/questions/39157781/the-configuration-file-appsettings-json-was-not-found-and-is-not-optional +- https://www.learndapper.com/non-query#dapper-execute +- https://learn.microsoft.com/en-us/dotnet/api/system.timespan?view=net-10.0 +- https://spectreconsole.net/console \ No newline at end of file