diff --git a/CodingTracker.DzemalKurtic/CodingTracker.DzemalKurtic.slnx b/CodingTracker.DzemalKurtic/CodingTracker.DzemalKurtic.slnx new file mode 100644 index 00000000..bbdb9534 --- /dev/null +++ b/CodingTracker.DzemalKurtic/CodingTracker.DzemalKurtic.slnx @@ -0,0 +1,3 @@ + + + diff --git a/CodingTracker.DzemalKurtic/CodingTracker.DzemalKurtic/CodingTracker.DzemalKurtic.csproj b/CodingTracker.DzemalKurtic/CodingTracker.DzemalKurtic/CodingTracker.DzemalKurtic.csproj new file mode 100644 index 00000000..2aaf3bf6 --- /dev/null +++ b/CodingTracker.DzemalKurtic/CodingTracker.DzemalKurtic/CodingTracker.DzemalKurtic.csproj @@ -0,0 +1,23 @@ + + + + Exe + net10.0 + enable + enable + + + + + + + + + + + + PreserveNewest + + + + diff --git a/CodingTracker.DzemalKurtic/CodingTracker.DzemalKurtic/Controllers/CodingSessionController.cs b/CodingTracker.DzemalKurtic/CodingTracker.DzemalKurtic/Controllers/CodingSessionController.cs new file mode 100644 index 00000000..d9cc117e --- /dev/null +++ b/CodingTracker.DzemalKurtic/CodingTracker.DzemalKurtic/Controllers/CodingSessionController.cs @@ -0,0 +1,73 @@ +using CodingTracker.DzemalKurtic.Models; +using Dapper; +using Microsoft.Data.Sqlite; + +namespace CodingTracker.DzemalKurtic.Controllers; + +internal class CodingSessionController +{ + public string ConnectionString { get; set; } + public CodingSessionController(string connectionString) + { + ConnectionString = connectionString; + } + + public void AddItem(DateTime start, DateTime end) + { + using var connection = new SqliteConnection(ConnectionString); + connection.Open(); + + var sql = + """ + INSERT INTO coding_sessions + (StartTime, EndTime) + VALUES (@StartTime, @EndTime) + """; + + var session = new CodingSession { StartTime = start, EndTime = end }; + + connection.Execute(sql, session); + } + + public int DeleteItem(int id) + { + using var connection = new SqliteConnection(ConnectionString); + connection.Open(); + + var sql = + """ + DELETE FROM coding_sessions + WHERE Id = @Id; + """; + + var rowCount = connection.Execute(sql, new { Id = id }); + return rowCount; + } + + public int UpdateItem(int id, DateTime start, DateTime end) + { + using var connection = new SqliteConnection(ConnectionString); + connection.Open(); + + var sql = + """ + UPDATE coding_sessions SET StartTime = @StartTime, EndTime = @EndTime + WHERE Id = @Id + """; + var session = new CodingSession { Id = id, StartTime = start, EndTime = end }; + + var rowCount = connection.Execute(sql, session); + return rowCount; + } + + public List ViewItems() + { + using var connection = new SqliteConnection(ConnectionString); + connection.Open(); + + var sql = "SELECT * FROM coding_sessions"; + var sessions = connection.Query(sql).ToList(); + + return sessions; + } +} diff --git a/CodingTracker.DzemalKurtic/CodingTracker.DzemalKurtic/Data/Database.cs b/CodingTracker.DzemalKurtic/CodingTracker.DzemalKurtic/Data/Database.cs new file mode 100644 index 00000000..146c9a43 --- /dev/null +++ b/CodingTracker.DzemalKurtic/CodingTracker.DzemalKurtic/Data/Database.cs @@ -0,0 +1,24 @@ +using Dapper; +using Microsoft.Data.Sqlite; + +namespace CodingTracker.DzemalKurtic.Data; + +internal static class Database +{ + public static void Initialize(string connectionString) + { + using var connection = new SqliteConnection(connectionString); + connection.Open(); + + var sql = + """ + CREATE TABLE IF NOT EXISTS coding_sessions ( + Id INTEGER PRIMARY KEY AUTOINCREMENT, + StartTime TEXT NOT NULL, + EndTime TEXT NOT NULL + ); + """; + + connection.Execute(sql); + } +} diff --git a/CodingTracker.DzemalKurtic/CodingTracker.DzemalKurtic/MenuAction.cs b/CodingTracker.DzemalKurtic/CodingTracker.DzemalKurtic/MenuAction.cs new file mode 100644 index 00000000..a72d6f87 --- /dev/null +++ b/CodingTracker.DzemalKurtic/CodingTracker.DzemalKurtic/MenuAction.cs @@ -0,0 +1,9 @@ +namespace CodingTracker.DzemalKurtic; + +internal enum MenuAction +{ + ViewSessions, + AddSession, + UpdateSession, + DeleteSession +} diff --git a/CodingTracker.DzemalKurtic/CodingTracker.DzemalKurtic/Models/CodingSession.cs b/CodingTracker.DzemalKurtic/CodingTracker.DzemalKurtic/Models/CodingSession.cs new file mode 100644 index 00000000..7d74c070 --- /dev/null +++ b/CodingTracker.DzemalKurtic/CodingTracker.DzemalKurtic/Models/CodingSession.cs @@ -0,0 +1,28 @@ +namespace CodingTracker.DzemalKurtic.Models; + +public class CodingSession +{ + public int Id { get; set; } + public DateTime StartTime { get; set; } + public DateTime EndTime { get; set; } + + public TimeSpan Duration => EndTime - StartTime; + + public CodingSession(int id, DateTime startTime, DateTime endTime) + { + Id = id; + StartTime = startTime; + EndTime = endTime; + } + + public CodingSession(DateTime startTime, DateTime endTime) + { + StartTime = startTime; + EndTime = endTime; + } + + public CodingSession() + { + + } +} diff --git a/CodingTracker.DzemalKurtic/CodingTracker.DzemalKurtic/Program.cs b/CodingTracker.DzemalKurtic/CodingTracker.DzemalKurtic/Program.cs new file mode 100644 index 00000000..1003bcf3 --- /dev/null +++ b/CodingTracker.DzemalKurtic/CodingTracker.DzemalKurtic/Program.cs @@ -0,0 +1,15 @@ +using CodingTracker.DzemalKurtic.Controllers; +using CodingTracker.DzemalKurtic.Data; +using CodingTracker.DzemalKurtic.Views; +using Microsoft.Extensions.Configuration; + +IConfiguration config = new ConfigurationBuilder() + .AddJsonFile("appsettings.json") + .Build(); + +var connectionString = config.GetConnectionString("DefaultConnection"); + +Database.Initialize(connectionString); +var controller = new CodingSessionController(connectionString); +var ui = new UserInterface(controller); +ui.MainMenu(); \ No newline at end of file diff --git a/CodingTracker.DzemalKurtic/CodingTracker.DzemalKurtic/Properties/launchSettings.json b/CodingTracker.DzemalKurtic/CodingTracker.DzemalKurtic/Properties/launchSettings.json new file mode 100644 index 00000000..f7bc628d --- /dev/null +++ b/CodingTracker.DzemalKurtic/CodingTracker.DzemalKurtic/Properties/launchSettings.json @@ -0,0 +1,7 @@ +{ + "profiles": { + "CodingTracker.DzemalKurtic": { + "commandName": "Project" + } + } +} \ No newline at end of file diff --git a/CodingTracker.DzemalKurtic/CodingTracker.DzemalKurtic/README.md b/CodingTracker.DzemalKurtic/CodingTracker.DzemalKurtic/README.md new file mode 100644 index 00000000..9589cc56 --- /dev/null +++ b/CodingTracker.DzemalKurtic/CodingTracker.DzemalKurtic/README.md @@ -0,0 +1,69 @@ +**Project requirements:** + + + +* Logging occurrence of a coding session +* Users need to be bale to input the start date of the coding session +* Users need to be bale to input the end date of the coding session +* Duration of the sessions should be calculated from these values +* App should use a real database +* Users should be able to insert, delete, update and view their coding sessions +* All input errors should be handled +* Only Dapper should be used + + + +**How the App works?** + + + +When a user starts the app, he sees a menu with options: + + + +\- View all Sessions + +\- Add a Session + +\- Update a Session + +\- Delete a Session + + + + + +Choosing "View all Sessions" will show him a table with all the sessions that are recorded in the database. + + + +Choosing "Add a Session" will open another screen where he will be able to enter a start time in dd-mm-yy hh:mm format. + +After entering it he will be able to enter a end time in dd-mm-yy hh:mm format for the session. + + + +Choosing "Update a Session" will bring up a new screen where a user will be able to update an existing record. + +You must use Id of the record that exists in the database. + +After choosing an Id user will be able to enter new start time and time for the session. + + + +Choosing "Delete a Session" bring up a new screen where a use will be able to delete the sessions that are already recorded. + +You must use Id of the record that exists in the database. + + + + + +**Thoughts on the project:** + + + +This project made me use appsettings.json where I put the connection string to the Sqlite database. Dapper is easier to use than ADO.NET. For user interface I used spectre console, which has quite a few neat features. I really like the table feature. Also it's easier to get a list of options and a correct answer. + +Hardest part was figuring out how to convert between a model and a database because Sqlite doesn't have a date data type. I was using strings in the model because I couldn't get it to work with DateTime type. But then I was wondering how to calculate the duration of the session. + diff --git a/CodingTracker.DzemalKurtic/CodingTracker.DzemalKurtic/Validation.cs b/CodingTracker.DzemalKurtic/CodingTracker.DzemalKurtic/Validation.cs new file mode 100644 index 00000000..e1068778 --- /dev/null +++ b/CodingTracker.DzemalKurtic/CodingTracker.DzemalKurtic/Validation.cs @@ -0,0 +1,40 @@ +using Spectre.Console; +using System.Globalization; + +namespace CodingTracker.DzemalKurtic; + +internal static class Validation +{ + internal static DateTime ValidateDate(string dateInput, string dateName) + { + string date = dateInput; + + while (!DateTime.TryParseExact(date, "dd-MM-yy HH:mm", CultureInfo.InvariantCulture, DateTimeStyles.None, out _)) + { + AnsiConsole.WriteLine("Invalid date. (Format: dd-mm-yy hh:mm). Try again:"); + date = AnsiConsole.Ask($"Enter the {dateName} date of the Coding Session: (Format: dd-mm-yy hh:mm)"); + } + + return DateTime.ParseExact(date, "dd-MM-yy HH:mm", CultureInfo.InvariantCulture); + } + + + internal static int ValidateId(int idInput) + { + int id = idInput; + while (Convert.ToInt32(id) < 0) + { + AnsiConsole.WriteLine("Number can't be negative. Try again."); + id = AnsiConsole.Ask("Please typt the Id od the item you want to update"); + } + + return Convert.ToInt32(id); + } + + internal static bool ValidateTimespan(DateTime start, DateTime end) + { + bool isBigger = end <= start; + if (isBigger) AnsiConsole.WriteLine("End date can not be before start date"); + return isBigger; + } +} diff --git a/CodingTracker.DzemalKurtic/CodingTracker.DzemalKurtic/Views/UserInterface.cs b/CodingTracker.DzemalKurtic/CodingTracker.DzemalKurtic/Views/UserInterface.cs new file mode 100644 index 00000000..cf249e67 --- /dev/null +++ b/CodingTracker.DzemalKurtic/CodingTracker.DzemalKurtic/Views/UserInterface.cs @@ -0,0 +1,166 @@ +using CodingTracker.DzemalKurtic.Controllers; +using Spectre.Console; + +namespace CodingTracker.DzemalKurtic.Views; + +internal class UserInterface +{ + private readonly CodingSessionController _codingSessionController; + + internal UserInterface(CodingSessionController controller) + { + _codingSessionController = controller; + } + + internal void MainMenu() + { + bool appRunning = true; + while (appRunning) + { + Console.Clear(); + + var choice = AnsiConsole.Prompt( + new SelectionPrompt() + .Title("What do you want to do?") + .AddChoices(Enum.GetValues()) + .UseConverter(action => action switch + { + MenuAction.ViewSessions => "View all Sessions", + MenuAction.AddSession => "Add a Session", + MenuAction.UpdateSession => "Update a Session", + MenuAction.DeleteSession => "Delete a Session", + })); + + switch (choice) + { + case MenuAction.ViewSessions: + ShowItems(); + break; + case MenuAction.AddSession: + AddItem(); + break; + case MenuAction.UpdateSession: + UpdateItem(); + break; + case MenuAction.DeleteSession: + DeleteItem(); + break; + } + } + } + + internal void ShowItems() + { + var table = new Table(); + table.Border(TableBorder.Rounded); + + table.AddColumn("ID"); + table.AddColumn("Start Time"); + table.AddColumn("End Time"); + table.AddColumn("Duration"); + + var sessions = _codingSessionController.ViewItems(); + + foreach (var session in sessions) + { + table.AddRow( + session.Id.ToString(), + $"[cyan]{session.StartTime:dd-MM-yyyy HH:mm}[/]", + $"[yellow]{session.EndTime:dd-MM-yyyy HH:mm}[/]", + $"[green]{session.Duration.Hours} hours {session.Duration.Minutes} minutes[/]" + ); + } + + AnsiConsole.Write(table); + AnsiConsole.MarkupLine("Press Any Key to Continue."); + Console.ReadKey(); + } + + internal void AddItem() + { + var start = getDate("start"); + var startDate = Validation.ValidateDate(start, "start"); + + var end = getDate("end"); + var endDate = Validation.ValidateDate(end, "end"); + + var isBigger = Validation.ValidateTimespan(startDate, endDate); + + if (!isBigger) + { + _codingSessionController.AddItem(startDate, endDate); + AnsiConsole.MarkupLine("Press Any Key to Continue."); + Console.ReadKey(); + } + else + { + AnsiConsole.MarkupLine("Press Any Key to Continue."); + Console.ReadKey(); + + Console.Clear(); + AddItem(); + } + + } + + internal void UpdateItem() + { + ShowItems(); + + var itemId = getId("update"); + var id = Validation.ValidateId(itemId); + + var start = getDate("start"); + var startDate = Validation.ValidateDate(start, "start"); + + var end = getDate("end"); + var endDate = Validation.ValidateDate(end, "end"); + + var rowCount = _codingSessionController.UpdateItem(itemId, startDate, endDate); + if (rowCount == 0) + { + AnsiConsole.MarkupLine($"Session with id {id} doesn't exist."); + AnsiConsole.MarkupLine("Press Any Key to Continue."); + Console.ReadKey(); + Console.Clear(); + MainMenu(); + }else + { + AnsiConsole.MarkupLine("Press Any Key to Continue."); + Console.ReadKey(); + } + } + + internal void DeleteItem() + { + ShowItems(); + + var itemId = getId("delete"); + var id = Validation.ValidateId(itemId); + + var rowCount = _codingSessionController.DeleteItem(id); + if (rowCount == 0) + { + AnsiConsole.MarkupLine($"Session with id {id} doesn't exist.\n"); + AnsiConsole.MarkupLine("Press Any Key to Continue.\n"); + Console.ReadKey(); + Console.Clear(); + DeleteItem(); + } + else + { + AnsiConsole.MarkupLine("Press Any Key to Continue."); + Console.ReadKey(); + } + } + + private string getDate(string time) + { + return AnsiConsole.Ask($"Enter the {time} time of the Coding Session: (Format: dd-mm-yy HH:mm)"); + } + + private int getId(string action) + { + return AnsiConsole.Ask($"Please typt the Id od the item you want to {action}"); + } +} diff --git a/CodingTracker.DzemalKurtic/CodingTracker.DzemalKurtic/appsettings.json b/CodingTracker.DzemalKurtic/CodingTracker.DzemalKurtic/appsettings.json new file mode 100644 index 00000000..04d1f510 --- /dev/null +++ b/CodingTracker.DzemalKurtic/CodingTracker.DzemalKurtic/appsettings.json @@ -0,0 +1,5 @@ +{ + "ConnectionStrings": { + "DefaultConnection" : "Data Source=coding-Tracker.db" + } +} \ No newline at end of file