diff --git a/CodingTracker.Endofficial/CodingTracker/CodingTracker.csproj b/CodingTracker.Endofficial/CodingTracker/CodingTracker.csproj new file mode 100644 index 000000000..be0508d9e --- /dev/null +++ b/CodingTracker.Endofficial/CodingTracker/CodingTracker.csproj @@ -0,0 +1,40 @@ + + + + Exe + net9.0 + enable + enable + + + + + + + + + + + + + + + + + + + + + + + + + Always + + + + + + + + diff --git a/CodingTracker.Endofficial/CodingTracker/CodingTracker.slnx b/CodingTracker.Endofficial/CodingTracker/CodingTracker.slnx new file mode 100644 index 000000000..b1ef0a179 --- /dev/null +++ b/CodingTracker.Endofficial/CodingTracker/CodingTracker.slnx @@ -0,0 +1,4 @@ + + + + diff --git a/CodingTracker.Endofficial/CodingTracker/Controller/CodingController.cs b/CodingTracker.Endofficial/CodingTracker/Controller/CodingController.cs new file mode 100644 index 000000000..66fea340d --- /dev/null +++ b/CodingTracker.Endofficial/CodingTracker/Controller/CodingController.cs @@ -0,0 +1,327 @@ +using Spectre.Console; +using CodingTracker; +using Dapper; +using Microsoft.Data.Sqlite; +using CodingTracker.Data; +using System.Data.SQLite; +using CodingTracker.Model; +using System.Diagnostics; + +namespace CodingTracker.Controller; + +internal class CodingController : Database +{ + public static bool LiveSession() + { + AnsiConsole.Clear(); + AnsiConsole.MarkupLine("[Aquamarine3]Start a live coding session.\n[/]"); + + string dateSession = InputInsert.GetDateSessionInput(); + if (dateSession == "0") return false; + + string input = AnsiConsole.Ask("[bold]\nPress 'P' to start the session.[/][yellow]Type 0 to return to main menu.[/]"); + if (input.ToUpper() == "P") + { + var durationSession = InputInsert.StopwatchSession(dateSession); + + if (durationSession != null) + { + using var connection = GetConnection(); + + string sql = @" + INSERT INTO CodingSessions (StartTime, EndTime, Date, Duration, Description) + VALUES (@StartTime, @EndTime, @Date, @Duration, @Description)"; + + connection.Execute(sql, durationSession); + } + } + + return true; + } + + public static bool RegisterSession() + { + AnsiConsole.Clear(); + + AnsiConsole.MarkupLine("[Aquamarine3]Register a new session.[/]\n"); + + string dateSession = InputInsert.GetDateSessionInput(); + if (dateSession == "0") return false; + + var durationSession = InputInsert.GetTimeSessionInput(dateSession); + if (durationSession == null) return false; + + if (durationSession != null) + { + using var connection = GetConnection(); + + string sql = @" + INSERT INTO CodingSessions (StartTime, EndTime, Date, Duration, Description) + VALUES (@StartTime, @EndTime, @Date, @Duration, @Description)"; + + connection.Execute(sql, durationSession); + } + + return true; + } + + public static bool ViewSessions() + { + AnsiConsole.Clear(); + + using var connection = GetConnection(); + + string sql = @" + SELECT * FROM CodingSessions"; + + var sessions = connection.Query(sql).ToList(); // Execute the query and map results to CodingSessions objects + + var table = new Table(); + table.Border(TableBorder.Rounded); + + table.AddColumn("[yellow]ID[/]"); + table.AddColumn("[yellow]Start Time[/]"); + table.AddColumn("[yellow]End Time[/]"); + table.AddColumn("[yellow]Date[/]"); + table.AddColumn("[yellow]Duration[/]"); + table.AddColumn("[yellow]Description[/]"); + + foreach (var session in sessions) + { + table.AddRow( + session.Id.ToString(), + session.StartTime.ToString("HH:mm"), + session.EndTime.ToString("HH:mm"), + session.Date, + session.Duration.ToString(@"hh\:mm"), + (session.Description ?? "Empty").ToString() + ); + } + + AnsiConsole.Write(table); + var action = AnsiConsole.Ask("[yellow]Press 'T' to order records or press '0' to return to main menu.[/]").Trim().ToUpper(); + + if (action == "0") return false; + else InputInsert.OrderSession(); + + AnsiConsole.MarkupLine("\n[yellow]Press any key to continue...[/]"); + Console.ReadKey(); + + return true; + } + + public static bool UpdateSession() + { + AnsiConsole.Clear(); + + AnsiConsole.MarkupLine("[Aquamarine3]Update a session.[/]\n"); + + using var connection = GetConnection(); + + string sql = @" + SELECT * FROM CodingSessions"; + + List tableData = new List(); + + var sessions = connection.Query(sql).ToList(); // Execute the query and map results to CodingSessions objects + + var table = new Table(); + table.Border(TableBorder.Rounded); + + table.AddColumn("[yellow]ID[/]"); + table.AddColumn("[yellow]Start Time[/]"); + table.AddColumn("[yellow]End Time[/]"); + table.AddColumn("[yellow]Date[/]"); + table.AddColumn("[yellow]Duration[/]"); + table.AddColumn("[yellow]Description[/]"); + + foreach (var session in sessions) + { + table.AddRow( + session.Id.ToString(), + session.StartTime.ToString("HH:mm"), + session.EndTime.ToString("HH:mm"), + session.Date, + session.Duration.ToString(@"hh\:mm"), + (session.Description ?? "Empty").ToString() + ); + } + + AnsiConsole.Write(table); + + int NumberId = InputInsert.GetId(); + if (NumberId == 0) return false; + + string sqlId = @" + SELECT EXISTS (SELECT 1 FROM CodingSessions WHERE Id = @Id)"; + + bool exists = connection.ExecuteScalar(sqlId, new { Id = NumberId }); + + while (!exists) + { + AnsiConsole.MarkupLine("[red]\nRecord not found![/]\n"); + + NumberId = InputInsert.GetId(); + if (NumberId == 0) return false; + + exists = connection.ExecuteScalar(sqlId, new { Id = NumberId }); + } + + string upInput = AnsiConsole.Ask("\n[bold]Type 1 if you want update the start time.\nType 2 to update the end time.\nType 3 to update the date.\nType 4 to update the description.\n[/][yellow]Type 0 to return to main menu.[/]"); + if (upInput == "0") return false; + + while (!Int32.TryParse(upInput, out _) || Convert.ToInt32(upInput) < 0 || Convert.ToInt32(upInput) > 4) + { + AnsiConsole.MarkupLine("[red]\nInvalid input! Please enter a valid number.[/]\n"); + upInput = AnsiConsole.Ask("\n[bold]Type 1 if you want update the start time.\nType 2 to update the end time.\nType 3 to update the date.\nType 4 to update the description.\n[/][yellow]Type 0 to return to main menu.[/]"); + if (upInput == "0") return false; + } + + switch (upInput) + { + case "1": + string startTime = InputInsert.OnlyStartTime(); + if (startTime == "0") return false; + + string sqlUpStartTime = @" + UPDATE CodingSessions SET StartTime = @StartTime WHERE Id = @Id"; + + connection.ExecuteScalar(sqlUpStartTime, new { Id = NumberId, StartTime = startTime }); + + AnsiConsole.MarkupLine("[green]\nStart time updated successfully![/]"); + AnsiConsole.MarkupLine("[yellow]Press any key to continue...[/]"); + ReadKey(); + break; + case "2": + string endTime = InputInsert.OnlyEndTime(); + if (endTime == "0") return false; + + string sqlUpEndTime = @" + UPDATE CodingSessions SET EndTime = @EndTime WHERE Id = @Id"; + + connection.ExecuteScalar(sqlUpEndTime, new { Id = NumberId, EndTime = endTime }); + + AnsiConsole.MarkupLine("[green]\nEnd time updated successfully![/]"); + AnsiConsole.MarkupLine("[yellow]Press any key to continue...[/]"); + ReadKey(); + break; + case "3": + string Date = InputInsert.GetDateSessionInput(); + if (Date == "0") return false; + + string sqlUpDate = @" + UPDATE CodingSessions SET Date = @Date WHERE Id = @Id"; + + connection.ExecuteScalar(sqlUpDate, new { Id = NumberId, Date = Date }); + + AnsiConsole.MarkupLine("[green]\nDate updated successfully![/]"); + AnsiConsole.MarkupLine("[yellow]Press any key to continue...[/]"); + ReadKey(); + break; + case "4": + string Description = InputInsert.OnlyDescription(); + + string sqlUpDescription = @" + UPDATE CodingSessions SET Description = @Description WHERE Id = @Id"; + + connection.ExecuteScalar(sqlUpDescription, new { Id = NumberId, Description = Description }); + + AnsiConsole.MarkupLine("[green]\nDescription updated successfully![/]"); + AnsiConsole.MarkupLine("[yellow]Press any key to continue...[/]"); + ReadKey(); + break; + } + + return true; + } + + public static bool DeleteSession() + { + AnsiConsole.Clear(); + + AnsiConsole.MarkupLine("[Aquamarine3]Delete a session.[/]\n"); + + using var connection = GetConnection(); + + string sql = @" + SELECT * FROM CodingSessions"; + + List tableData = new List(); + + var sessions = connection.Query(sql).ToList(); // Execute the query and map results to CodingSessions objects + + if (sessions.Count == 0) + { + AnsiConsole.MarkupLine("[red]No sessions found![/]"); + AnsiConsole.MarkupLine("[yellow]Press any key to continue...[/]"); + ReadKey(); + return false; + } + + var table = new Table(); + table.Border(TableBorder.Rounded); + + table.AddColumn("[yellow]ID[/]"); + table.AddColumn("[yellow]Start Time[/]"); + table.AddColumn("[yellow]End Time[/]"); + table.AddColumn("[yellow]Date[/]"); + table.AddColumn("[yellow]Duration[/]"); + table.AddColumn("[yellow]Description[/]"); + + foreach (var session in sessions) + { + table.AddRow( + session.Id.ToString(), + session.StartTime.ToString("HH:mm"), + session.EndTime.ToString("HH:mm"), + session.Date, + session.Duration.ToString(@"hh\:mm"), + (session.Description ?? "Empty").ToString() + ); + } + + AnsiConsole.Write(table); + + string delInput = AnsiConsole.Ask("\n[bold]Type 1 if you want delete all sessions.\nType 2 if you want delete a session.\n[/][yellow]Type 0 to return to main menu.[/]"); + if (delInput == "0") return false; + + while (!Int32.TryParse(delInput, out _) || Convert.ToInt32(delInput) < 0 || Convert.ToInt32(delInput) > 2) + { + AnsiConsole.MarkupLine("[red]Invalid input! Please enter a valid number.[/]\n"); + delInput = AnsiConsole.Ask("\n[bold]Type 1 if you want delete all sessions.\nType 2 if you want delete a session.\n[/][yellow]Type 0 to return to main menu.[/]"); + if (delInput == "0") return false; + } + + switch (delInput) + { + case "1": + string sqlDelAll = @" + DELETE FROM CodingSessions"; + + connection.Execute(sqlDelAll); + + AnsiConsole.MarkupLine("[red]\nAll sessions deleted![/]"); + AnsiConsole.MarkupLine("[yellow]\nPress any key to continue...[/]"); + ReadKey(); + + break; + case "2": + int delId = InputInsert.GetId(); + if (delId == 0) return false; + + string sqlDelSession = @" + DELETE FROM CodingSessions WHERE Id = @Id"; + + connection.Execute(sqlDelSession, new { Id = delId }); + + AnsiConsole.MarkupLine("[red]\nSession deleted![/]"); + AnsiConsole.MarkupLine("[yellow]\nPress any key to continue...[/]"); + ReadKey(); + break; + } + + return true; + } + +} + diff --git a/CodingTracker.Endofficial/CodingTracker/Controller/CodingFilterOrder.cs b/CodingTracker.Endofficial/CodingTracker/Controller/CodingFilterOrder.cs new file mode 100644 index 000000000..b36e1fcc0 --- /dev/null +++ b/CodingTracker.Endofficial/CodingTracker/Controller/CodingFilterOrder.cs @@ -0,0 +1,237 @@ +using CodingTracker.Data; +using CodingTracker.Model; +using Dapper; +using Spectre.Console; +using static CodingTracker.Enums; + +namespace CodingTracker.Controller; +internal class CodingFilterOrder : Database +{ + public static void OrderToYears() + { + AnsiConsole.Clear(); + + using var connection = GetConnection(); + + string sql = "SELECT DISTINCT strftime('%Y', Date) AS Year FROM CodingSessions ORDER BY Year ASC"; + + // conver to string in int + var years = connection.Query(sql); + if (!years.Any()) + { + AnsiConsole.MarkupLine("[red]Record not found![/]"); + return; + } + + var selectedYear = AnsiConsole.Prompt( + new SelectionPrompt() + .Title("Choice a year to view records:") + .PageSize(10) + .AddChoices(years) + ); + + var sqlDetails = "SELECT * FROM CodingSessions WHERE strftime('%Y', Date) = @Year"; + var sessions = connection.Query(sqlDetails, new { Year = selectedYear.ToString() }).ToList(); + + var table = new Table(); + table.Border(TableBorder.Rounded); + + table.AddColumn("Id"); + table.AddColumn("Start Time"); + table.AddColumn("End Time"); + table.AddColumn("Date"); + table.AddColumn("Duration"); + table.AddColumn("Description"); + + foreach (var session in sessions) + { + table.AddRow( + session.Id.ToString(), + session.StartTime.ToString("HH:mm"), + session.EndTime.ToString("HH:mm"), + session.Date, + session.Duration.ToString(@"hh\:mm"), + (session.Description ?? "Empty").ToString() + ); + } + + AnsiConsole.Write(table); + return; + } + + public static void OrderToMonths() + { + AnsiConsole.Clear(); + + var connection = GetConnection(); + + string sql = "SELECT DISTINCT strftime('%m', Date) As Month FROM CodingSessions ORDER BY Month ASC"; + + var months = connection.Query(sql).ToList(); + if (!months.Any()) + { + AnsiConsole.MarkupLine("[red]Record not found![/]"); + return; + } + + var selectedMonth = AnsiConsole.Prompt( + new SelectionPrompt() + .Title("Choice a month to view record: ") + .PageSize(10) + .AddChoices(months) + .UseConverter(m => + { + return System.Globalization.DateTimeFormatInfo.InvariantInfo.GetMonthName(m); + })); + + // use CAST to specify that valus is an int + string sqlDetails = "SELECT * FROM CodingSessions WHERE CAST(strftime('%m', Date) AS INT) = @Month"; + var sessions = connection.Query(sqlDetails, new { Month = selectedMonth.ToString() }).ToList(); + + var table = new Table(); + table.Border(TableBorder.Rounded); + + table.AddColumn("Id"); + table.AddColumn("Start Time"); + table.AddColumn("End Time"); + table.AddColumn("Date"); + table.AddColumn("Duration"); + table.AddColumn("Description"); + + foreach (var session in sessions) + { + table.AddRow( + session.Id.ToString(), + session.StartTime.ToString("HH:mm"), + session.EndTime.ToString("HH:mm"), + session.Date, + session.Duration.ToString(@"hh\:mm"), + (session.Description ?? "Empty").ToString() + ); + } + + AnsiConsole.Write(table); + } + + public static void OrderToDays() + { + AnsiConsole.Clear(); + + var connection = GetConnection(); + + // 'w' => 0: Sunday; 1: Monday... + string sql = "SELECT DISTINCT strftime ('%w', date) AS Day FROM CodingSessions ORDER BY Day ASC"; + + var day = connection.Query(sql).ToList(); + if (!day.Any()) + { + AnsiConsole.MarkupLine("Record not found!"); + return; + } + + var dayChoice = AnsiConsole.Prompt( + new SelectionPrompt() + .Title("Choice a day to view record: ") + .PageSize(10) + .AddChoices(day) + .UseConverter(m => + { + return System.Globalization.CultureInfo.InvariantCulture.DateTimeFormat.GetDayName((DayOfWeek)m); + })); + + string sqlDetails = "SELECT * FROM CodingSessions WHERE CAST(strfTime('%w', date)AS INT) = @Day"; + var sessions = connection.Query(sqlDetails, new { Day = dayChoice.ToString() }).ToList(); + + var table = new Table(); + table.Border(TableBorder.Rounded); + + table.AddColumn("Id"); + table.AddColumn("Start Time"); + table.AddColumn("End Time"); + table.AddColumn("Date"); + table.AddColumn("Duration"); + table.AddColumn("Description"); + + foreach (var session in sessions) + { + table.AddRow( + session.Id.ToString(), + session.StartTime.ToString("HH:mm"), + session.EndTime.ToString("HH:mm"), + session.Date, + session.Duration.ToString(@"hh\:mm"), + (session.Description ?? "Empty").ToString()); + } + + AnsiConsole.Write(table); + return; + } + + public static void AscendingOrder() + { + AnsiConsole.Clear(); + + var connection = GetConnection(); + + string sql = "SELECT * FROM CodingSessions ORDER BY Date ASC"; + var sessions = connection.Query(sql).ToList(); + + var table = new Table(); + table.Border(TableBorder.Rounded); + + table.AddColumn("[yellow]ID[/]"); + table.AddColumn("[yellow]Start Time[/]"); + table.AddColumn("[yellow]End Time[/]"); + table.AddColumn("[yellow]Date[/]"); + table.AddColumn("[yellow]Duration[/]"); + table.AddColumn("[yellow]Description[/]"); + + foreach (var session in sessions) + { + table.AddRow( + session.Id.ToString(), + session.StartTime.ToString("HH:mm"), + session.EndTime.ToString("HH:mm"), + session.Date, + session.Duration.ToString(@"hh\:mm"), + (session.Description ?? "Empty").ToString() + ); + } + + AnsiConsole.Write(table); + } + + public static void DescendingOrder() + { + AnsiConsole.Clear(); + + var connection = GetConnection(); + + string sql = "SELECT * FROM CodingSessions ORDER BY Date DESC"; + var sessions = connection.Query(sql).ToList(); + + var table = new Table(); + table.Border(TableBorder.Rounded); + + table.AddColumn("[yellow]ID[/]"); + table.AddColumn("[yellow]Start Time[/]"); + table.AddColumn("[yellow]End Time[/]"); + table.AddColumn("[yellow]Date[/]"); + table.AddColumn("[yellow]Duration[/]"); + table.AddColumn("[yellow]Description[/]"); + + foreach (var session in sessions) + { + table.AddRow( + session.Id.ToString(), + session.StartTime.ToString("HH:mm"), + session.EndTime.ToString("HH:mm"), + session.Date, + session.Duration.ToString(@"hh\:mm"), + (session.Description ?? "Empty").ToString() + ); + } + + AnsiConsole.Write(table); + } +} diff --git a/CodingTracker.Endofficial/CodingTracker/Controller/InputInsert.cs b/CodingTracker.Endofficial/CodingTracker/Controller/InputInsert.cs new file mode 100644 index 000000000..cab35b779 --- /dev/null +++ b/CodingTracker.Endofficial/CodingTracker/Controller/InputInsert.cs @@ -0,0 +1,305 @@ +using CodingTracker.Model; +using Spectre.Console; +using System.Diagnostics; +using System.Globalization; +using static CodingTracker.Enums; + +namespace CodingTracker.Controller; + +public class InputInsert +{ + public static CodingSessions StopwatchSession(string sessionDate) + { + Clear(); + string description = AnsiConsole.Ask("Please enter a description for the session."); + + AnsiConsole.MarkupLine("\n[green]Session started![/]"); + AnsiConsole.MarkupLine("\n[yellow]Press any key to stop session.[/]"); + + var stopwatch = Stopwatch.StartNew(); //initialize to use a stopwatch + DateTime startTime = DateTime.Now; + + // Display a status message while the session is in progress + // .status make a status message that can be updated while the session is running, + // and .spinner adds a spinner animation to indicate that something is happening in the background. + AnsiConsole.Status() + .Spinner(Spinner.Known.Clock) + .Start("Live session in progress...", ctx => + { + stopwatch.Start(); // Start the stopwatch + + while (!KeyAvailable) + { + var elapsed = stopwatch.Elapsed; // Get the elapsed time since the session started + + string time = string.Format("{0:00}:{1:00}:{2:00}", + elapsed.Hours, elapsed.Minutes, elapsed.Seconds); + + // to update a message + ctx.Status($"[blue]Stopwatch running:[/] {time}"); + + // add a delay to safe CPU + // Don't act fot 50ms => 20 FPS + Thread.Sleep(50); + + } + ReadKey(true); + }); + + stopwatch.Stop(); + + DateTime endTime = DateTime.Now; + TimeSpan duration = endTime - startTime; + string[] formatsTime = { "H\\:mm", "HH\\:mm" }; + + TimeOnly StartTime = TimeOnly.FromDateTime(startTime); + TimeOnly EndTime = TimeOnly.FromDateTime(endTime); + + var session = new CodingSessions(0, StartTime, EndTime, sessionDate, duration, description); + + Clear(); + AnsiConsole.MarkupLine("[red]Session stopped![/]\n[yellow]Press any key to continue...[/]"); + ReadKey(); + + return session; + } + + // IAnsiConsole is used to test with spectre.console.testing + public static string GetDateSessionInput(IAnsiConsole? console = null) + { + var _console = console ?? AnsiConsole.Console; + + var date = _console.Ask("Please enter date (yyyy-MM-dd). [yellow]You type 0 to return to main menu.[/]\n").Trim(); + if (date == "0") return "0"; + + while (!DateTime.TryParseExact(date, "yyyy-MM-dd", CultureInfo.InvariantCulture, DateTimeStyles.None, out _)) + { + _console.MarkupLine("[red]Invalid date format.[/]\n"); + date = _console.Ask("Please enter date (yyyy-MM-dd). You type [yellow]0 to return to main menu.\n[/]").Trim(); + if (date == "0") return "0"; + } + + _console.MarkupLine($"[green]Date registered!\n[/]"); + return date; + } + + public static CodingSessions? GetTimeSessionInput(string sessionDate, IAnsiConsole? console = null) + { + var _console = console ?? AnsiConsole.Console; + + string[] formats = { @"h\:mm", @"hh\:mm" }; + string description = _console.Ask("Please enter a description for the session."); + + string startInput = _console.Prompt( + new TextPrompt("[bold]\nPlease insert the start time (Format: [green]HH:mm[/]) or type [yellow]0[/] to return to main menu.[/]") + .Validate(input => + { + var cleanInput = input.Trim(); + if (cleanInput == "0") return ValidationResult.Success(); + + // if the format is valid, it will be stored in the variable time, otherwise it will return false + bool isValid = TimeSpan.TryParseExact(cleanInput, formats, CultureInfo.InvariantCulture, out var time); + + // Check if the time is valid and within the range of 0 to 24 hours + if (!isValid) return ValidationResult.Error("[red]Time invalid! Use the time format '[blue]HH:mm[/]'[/]"); + + if (time.Ticks < 0) return ValidationResult.Error("[red]Negative time not allowed.[/]"); + + return ValidationResult.Success(); + })); + if (startInput.Trim() == "0") return null!; + + string endInput = _console.Prompt( + new TextPrompt("[bold]\nPlease insert the end time (Format: [green]HH:mm[/]) or type [yellow]0[/] to return to main menu.[/]") + .Validate(input => + { + var cleanInputEnd = input.Trim(); + if (cleanInputEnd == "0") return ValidationResult.Success(); + + bool isValid = TimeSpan.TryParseExact(cleanInputEnd, formats, CultureInfo.InvariantCulture, out var time); + + // Check if the time is valid and within the range of 0 to 24 hours + if (!isValid) return ValidationResult.Error("[red]Time invalid! Use the time format '[blue]hh:mm[/]'[/]"); + + if (time.Ticks < 0) return ValidationResult.Error("[red]Negative time not allowed.[/]"); + + return ValidationResult.Success(); + })); + if (endInput.Trim() == "0") return null!; + + // Define another because DateTime.TryParseExact doesn't accept TimeSpan formats, it needs to be converted to DateTime + string[] formatsTime = { "H\\:mm", "HH\\:mm" }; + // convert string to DateTime + if (!TimeOnly.TryParseExact(startInput.Trim(), formatsTime, null!, DateTimeStyles.None, out TimeOnly resultStart)) return null!; + + if (!TimeOnly.TryParseExact(endInput.Trim(), formatsTime, null!, DateTimeStyles.None, out TimeOnly resultEnd)) return null!; + + // calculate duration + TimeSpan duration = resultEnd - resultStart; + + var session = new CodingSessions(0, resultStart, resultEnd, sessionDate, duration, description); + + session.DisplayConfirmRegister(); + + _console.MarkupLine("[yellow]Press any key to continue...[/]"); + _console.Input.ReadKey(true); + + return session; + } + + public static string OnlyStartTime() + { + string[] formats = { @"h\:mm", @"hh\:mm" }; + string startInput = AnsiConsole.Prompt( + new TextPrompt("[bold]\nPlease insert the start time (Format: [green]HH:mm[/]) or type [yellow]0[/] to return to main menu.[/]") + .Validate(input => + { + if (input == "0") return ValidationResult.Success(); + + // if the format is valid, it will be stored in the variable time, otherwise it will return false + bool isValid = TimeSpan.TryParseExact(input, formats, CultureInfo.InvariantCulture, out var time); + + // Check if the time is valid and within the range of 0 to 24 hours + if (!isValid) return ValidationResult.Error("[red]Time invalid! Use the time format '[blue]HH:mm[/]'[/]"); + + if (time.Ticks < 0) return ValidationResult.Error("[red]Negative time not allowed.[/]"); + + return ValidationResult.Success(); + })); + + if (startInput == "0") return null!; + + return startInput; + } + + public static string OnlyEndTime() + { + string[] formats = { @"h\:mm", @"hh\:mm" }; + string endInput = AnsiConsole.Prompt( + new TextPrompt("[bold]\nPlease insert the end time (Format: [green]HH:mm[/]) or type [yellow]0[/] to return to main menu.[/]") + .Validate(input => + { + if (input == "0") return ValidationResult.Success(); + + bool isValid = TimeSpan.TryParseExact(input, formats, CultureInfo.InvariantCulture, out var time); + + // Check if the time is valid and within the range of 0 to 24 hours + if (!isValid) return ValidationResult.Error("[red]Time invalid! Use the time format '[blue]hh:mm[/]'[/]"); + + if (time.Ticks < 0) return ValidationResult.Error("[red]Negative time not allowed.[/]"); + + return ValidationResult.Success(); + })); + if (endInput == "0") return null!; + + return endInput; + } + + public static string OnlyDescription(IAnsiConsole? console = null) + { + var _console = console ?? AnsiConsole.Console; + + string description = _console.Ask("\nPlease enter a description for the session.\n"); + return description; + } + + public static int GetId(IAnsiConsole? console = null) + { + var _console = console ?? AnsiConsole.Console; + + string numberId = _console.Ask("\nPlease enter the ID of the session. You type [yellow]0 to return to main menu.\n[/]").Trim(); + + if (numberId == "0") return 0; + + // Validate that the input is a positive integer or zero (to return to main menu) + while (!Int32.TryParse(numberId, out _) || Convert.ToInt32(numberId) < 0) + { + _console.MarkupLine("[red]Invalid ID. Please enter a positive integer.[/]\n"); + numberId = _console.Ask("Please enter the ID of the session. You type [yellow]0 to return to main menu.\n[/]").Trim(); + if (numberId == "0") return 0; + } + + int finalId = Convert.ToInt32(numberId); + return finalId; + } + + public static List RandomSession() + { + Random random = new Random(); + + List codSession = new List(); + string[] Description = { "Working", "Programming", "Playing" }; + + DateTime start = new DateTime(2020, 1, 1); + int range = (DateTime.Today - start).Days; + + for(int i = 0; i < 100; i++) + { + var randomStart = new TimeOnly(random.Next(0, 24), random.Next(0, 60), random.Next(0, 59)); + var randomEnd = new TimeOnly(random.Next(0, 24), random.Next(0, 60), random.Next(0, 59)); + string randomDate = start.AddDays(random.Next(range)).ToString("yyyy-MM-dd"); + + TimeSpan duration = randomEnd - randomStart; + + int desRandom = random.Next(0, Description.Length); + string description = Description[desRandom]; + + var session = new CodingSessions(0, randomStart, randomEnd, randomDate, duration, description); + codSession.Add(session); + } + + return codSession; + } + + public static FilterAction OrderSession() + { + Clear(); + + var filterChoice = AnsiConsole.Prompt( + new SelectionPrompt() + .Title("How do you want to visualize the data?") + .UseConverter(option => option switch + { + FilterAction.orderToYears => "Order to year", + FilterAction.orderToMonths => "Order to month", + FilterAction.orderToDays => "Order to day", + FilterAction.ascendingOrder => "Ascending order", + FilterAction.descendingOrder => "Descending order", + FilterAction.Exit => "[red]Close App[/]", + _ => option.ToString() + }) + .AddChoices(Enum.GetValues())); + + switch (filterChoice) + { + case FilterAction.orderToYears: + CodingFilterOrder.OrderToYears(); + break; + case FilterAction.orderToMonths: + CodingFilterOrder.OrderToMonths(); + break; + case FilterAction.orderToDays: + CodingFilterOrder.OrderToDays(); + break; + case FilterAction.ascendingOrder: + CodingFilterOrder.AscendingOrder(); + break; + case FilterAction.descendingOrder: + CodingFilterOrder.DescendingOrder(); + break; + case FilterAction.Exit: + AnsiConsole.Status() + .Spinner(Spinner.Known.Dots) + .Start("Data rescue...", ctx => + { + Thread.Sleep(1000); + }); + + Environment.Exit(0); + break; + } + + return filterChoice; + } +} + diff --git a/CodingTracker.Endofficial/CodingTracker/Controller/RandomValues.cs b/CodingTracker.Endofficial/CodingTracker/Controller/RandomValues.cs new file mode 100644 index 000000000..36460b2f5 --- /dev/null +++ b/CodingTracker.Endofficial/CodingTracker/Controller/RandomValues.cs @@ -0,0 +1,27 @@ +using Dapper; +using CodingTracker.Data; + +namespace CodingTracker.Controller; + +// To register some records +internal class RandomValues : Database +{ + public void ValueRandom() + { + using var connection = GetConnection(); + + long countRecord = connection.ExecuteScalar("SELECT COUNT (*) FROM CodingSessions"); + + if (countRecord == 0) + { + var randomValues = InputInsert.RandomSession(); + + string sql = @" + INSERT INTO CodingSessions (StartTime, EndTime, Date, Duration, Description) + VALUES (@StartTime, @EndTime, @Date, @Duration, @Description)"; + + connection.Execute(sql, randomValues); + } + } +} + diff --git a/CodingTracker.Endofficial/CodingTracker/Data/Database.cs b/CodingTracker.Endofficial/CodingTracker/Data/Database.cs new file mode 100644 index 000000000..122f5bfd8 --- /dev/null +++ b/CodingTracker.Endofficial/CodingTracker/Data/Database.cs @@ -0,0 +1,60 @@ +using CodingTracker.Model; +using Dapper; +using Microsoft.Data.Sqlite; +using Microsoft.Extensions.Configuration; +using Spectre.Console; +using System.Runtime.InteropServices.Marshalling; + +namespace CodingTracker.Data; + +internal class Database +{ + // string.empty is used to initialize the connection string variable with an empty string, ensuring that it has a default value before being assigned the actual connection string from the configuration file. + private static string _connectionString = string.Empty; + + public Database() + { + var config = new ConfigurationBuilder() + .AddJsonFile("appsettings.json", optional: false, reloadOnChange: true) + .Build(); + + // Retrieve the connection string from the configuration + _connectionString = config.GetConnectionString("DefaultConnection") ?? throw new InvalidOperationException("Connection string not found in configuration."); + } + + public static SqliteConnection GetConnection() + { + // Create and return a new SQLite connection using the connection string + return new SqliteConnection(_connectionString); + } + + public void Initialize() + { + AnsiConsole.Status() + .Start("Database initialization...", ctx => + { + using var connection = GetConnection(); + + string sql = @" + CREATE TABLE IF NOT EXISTS CodingSessions ( + Id INTEGER PRIMARY KEY AUTOINCREMENT, + StartTime TEXT NOT NULL, + EndTime TEXT NOT NULL, + Duration TEXT NOT NULL, + Date TEXT NOT NULL, + Description TEXT + );"; + + connection.Execute(sql); + + Thread.Sleep(2000); + + ctx.Status("Loading session..."); + ctx.Spinner(Spinner.Known.Clock); + ctx.SpinnerStyle(Style.Parse("yellow")); + + Thread.Sleep(2000); + }); + } +} + diff --git a/CodingTracker.Endofficial/CodingTracker/Data/TimeOnlyTypeHandler.cs b/CodingTracker.Endofficial/CodingTracker/Data/TimeOnlyTypeHandler.cs new file mode 100644 index 000000000..00f424846 --- /dev/null +++ b/CodingTracker.Endofficial/CodingTracker/Data/TimeOnlyTypeHandler.cs @@ -0,0 +1,21 @@ +using Dapper; +using System.Data; +using System.Globalization; +using System.Runtime.CompilerServices; + +namespace CodingTracker.Data; +public class TimeOnlyTypeHandler : SqlMapper.TypeHandler +{ + // This method is called when you send the information to the database + public override void SetValue(IDbDataParameter parameter, TimeOnly value) + { + parameter.Value = value.ToString("HH:mm", CultureInfo.InvariantCulture); // Store as string in the database + } + + // This method is called when you retrieve the information from the database + public override TimeOnly Parse(object value) + { + return TimeOnly.Parse((string)value); // Parse the string back to TimeOnly + } +} + diff --git a/CodingTracker.Endofficial/CodingTracker/Enums.cs b/CodingTracker.Endofficial/CodingTracker/Enums.cs new file mode 100644 index 000000000..6285bda14 --- /dev/null +++ b/CodingTracker.Endofficial/CodingTracker/Enums.cs @@ -0,0 +1,25 @@ +namespace CodingTracker; + +public class Enums +{ + internal enum MenuAction + { + LiveSession, + RegisterSession, + ViewSessions, + UpdateSession, + DeleteSession, + ExiSession + } + + public enum FilterAction + { + orderToYears, + orderToMonths, + orderToDays, + ascendingOrder, + descendingOrder, + Exit + } +} + diff --git a/CodingTracker.Endofficial/CodingTracker/Model/CodingSessions.cs b/CodingTracker.Endofficial/CodingTracker/Model/CodingSessions.cs new file mode 100644 index 000000000..ea6b473a7 --- /dev/null +++ b/CodingTracker.Endofficial/CodingTracker/Model/CodingSessions.cs @@ -0,0 +1,39 @@ +using Spectre.Console; +using System.Runtime.InteropServices; +using static System.Collections.Specialized.BitVector32; + +namespace CodingTracker.Model; + +public class CodingSessions +{ + public int Id { get; set; } + public TimeOnly StartTime { get; set; } + public TimeOnly EndTime { get; set; } + public string Date { get; set; } + public string? Description { get; set; } + + public TimeSpan Duration => EndTime - StartTime; + + public CodingSessions(int id, TimeOnly startTime, TimeOnly endTime, string date, TimeSpan duration, string? description) + { + Id = id; + StartTime = startTime; + EndTime = endTime; + Date = date; + duration = Duration; + Description = description; + } + + public CodingSessions() { } + + /*public void DisplaySession(int id, DateTime startTime, DateTime endTime, string date, TimeSpan duration, string? description) + { + AnsiConsole.MarkupLine($"\nSession registered: [green]{startTime:HH:mm} - {endTime:HH:mm}[/] with duration [blue]{duration:hh\\:mm}[/] and description: [yellow]{description}[/]. Date session: [green]{date}[/]"); + }*/ + + public void DisplayConfirmRegister() + { + AnsiConsole.MarkupLine("\n[green]Session registered![/]"); + } +} + diff --git a/CodingTracker.Endofficial/CodingTracker/Program.cs b/CodingTracker.Endofficial/CodingTracker/Program.cs new file mode 100644 index 000000000..28cdf777d --- /dev/null +++ b/CodingTracker.Endofficial/CodingTracker/Program.cs @@ -0,0 +1,24 @@ +using CodingTracker.Data; +using CodingTracker.Controller; +using Dapper; +using Spectre.Console; + +namespace CodingTracker +{ + internal class Program + { + static void Main(string[] args) + { + SqlMapper.AddTypeHandler(new TimeOnlyTypeHandler()); // Register the TimeOnly type handler with Dapper + + Database database = new(); + database.Initialize(); + + RandomValues values = new RandomValues(); + values.ValueRandom(); + + UserInterface ui = new(); + ui.MainMenu(); + } + } +} diff --git a/CodingTracker.Endofficial/CodingTracker/Readme.md b/CodingTracker.Endofficial/CodingTracker/Readme.md new file mode 100644 index 000000000..f1f682513 --- /dev/null +++ b/CodingTracker.Endofficial/CodingTracker/Readme.md @@ -0,0 +1,83 @@ +# CODING TRACKER + +Coding Tracker is my C# application. It is used to record working sessions, using SQlite. + +This application is CRUD (Create, Read, Update e Delete). + +## Key Features + +- **Session recording**: The program records new work sessions, recording date, a start time, a end time and storing a duration. +- **CRUD**: The program allows to add, read, update and delete the sessions. +- **Code details**: The code is written using Spectre.Console. +- **Store date**: The program communicate with a database, using SQlite with Dapper. +- **Database initialization**: If database don't exists, it is auto-generated automatically. +- **Self-genereting data**: If database is empty, 100 records are generated. +- **Error handling**: The code provides a robust error handling user generated and possible exceptions. +- **Unit test**: The code provides the unit tests to test the correct function of the methods. +- **Console UI**: Open the program, after loading the database, is shown a menu. The user can move the directional arrows to navigate in the menu. + + - menu + +## Functionality & Usage + +- **Live session** + - ***Date recording***: Record date of the session. + - date + + - ***User choice***: Type 'P' or 'p' to continue. Type '0' to return to main menu. + - play session + + - ***Description***: Add a description for the session. + - description session + + - ***Start session***: The stopwatch is start and it is stop when the user presses any key. + - start session + +- **Register a new session** + - ***Date recording***: Record date of the session. + - ***Description***: Add a description for the session. + - ***Start time***: Enter the start time. + - Start Time + + - ***End time***: Enter the end time. + - End Time + +- **View sessions** + - ***View of the sessions***: It's shown a table with all the sessions. + - All records + + - ***Fiters***: A list of filters is displayed + - view filters + + - ***Year filter***: The user can choose the year that he wants to view is displayed. + - filter to year + + - ***Month filter***: The user can choose the month that he wants to view is displayed. + - filter to month + + - ***Day filter***: The user can choose the day that he wants to view is displayed. + - filter to day + + - ***Ascending order filter***: The user can choose to view sessions by ascending order. + - ***Descending order filter***: The user can choose to view sessions by descending order. + +- **Update session** + - ***View of the sessions***: It's shown a table with all the sessions. + - ***What you want update?***: It's shown a list and the user can navigate to choose what to update. + + - view all sessions + +- **Delete session** + - ***View of the sessions***: It's shown a table with all the sessions. + - view all sessions to delete + + - ***Delete all database***: Type '1' to delete all database. + - ***Delete one session***: If the user press '2', he will have to indicate the Id session he wants to delete. + - option to delete one session + +## What I learned + +- Database integration: I learned how to create, connect and communicate with a database using Dapper. +- Stopwatch integration: I learned how to implement the stopwatch method to live session. +- Problem solving: I had difficulties to implement the filters. I solved it with a serch on the web. I understand the 'strftime' function. It is used to format date and time and convert in text. +- Unit testing: I implement of the unit tests to verify the correct function of the methods. \ No newline at end of file diff --git a/CodingTracker.Endofficial/CodingTracker/Resources/doc/images/Live-session-date.png b/CodingTracker.Endofficial/CodingTracker/Resources/doc/images/Live-session-date.png new file mode 100644 index 000000000..194aa6c1c Binary files /dev/null and b/CodingTracker.Endofficial/CodingTracker/Resources/doc/images/Live-session-date.png differ diff --git a/CodingTracker.Endofficial/CodingTracker/Resources/doc/images/Menu.png b/CodingTracker.Endofficial/CodingTracker/Resources/doc/images/Menu.png new file mode 100644 index 000000000..96455a930 Binary files /dev/null and b/CodingTracker.Endofficial/CodingTracker/Resources/doc/images/Menu.png differ diff --git a/CodingTracker.Endofficial/CodingTracker/Resources/doc/images/all-records.png b/CodingTracker.Endofficial/CodingTracker/Resources/doc/images/all-records.png new file mode 100644 index 000000000..ed051626b Binary files /dev/null and b/CodingTracker.Endofficial/CodingTracker/Resources/doc/images/all-records.png differ diff --git a/CodingTracker.Endofficial/CodingTracker/Resources/doc/images/all-sessions-update.png b/CodingTracker.Endofficial/CodingTracker/Resources/doc/images/all-sessions-update.png new file mode 100644 index 000000000..3312736c4 Binary files /dev/null and b/CodingTracker.Endofficial/CodingTracker/Resources/doc/images/all-sessions-update.png differ diff --git a/CodingTracker.Endofficial/CodingTracker/Resources/doc/images/allsessiontodelete.png b/CodingTracker.Endofficial/CodingTracker/Resources/doc/images/allsessiontodelete.png new file mode 100644 index 000000000..a1a3bd548 Binary files /dev/null and b/CodingTracker.Endofficial/CodingTracker/Resources/doc/images/allsessiontodelete.png differ diff --git a/CodingTracker.Endofficial/CodingTracker/Resources/doc/images/choice-list.png b/CodingTracker.Endofficial/CodingTracker/Resources/doc/images/choice-list.png new file mode 100644 index 000000000..795130217 Binary files /dev/null and b/CodingTracker.Endofficial/CodingTracker/Resources/doc/images/choice-list.png differ diff --git a/CodingTracker.Endofficial/CodingTracker/Resources/doc/images/choosedelete.png b/CodingTracker.Endofficial/CodingTracker/Resources/doc/images/choosedelete.png new file mode 100644 index 000000000..b1d92b856 Binary files /dev/null and b/CodingTracker.Endofficial/CodingTracker/Resources/doc/images/choosedelete.png differ diff --git a/CodingTracker.Endofficial/CodingTracker/Resources/doc/images/description.png b/CodingTracker.Endofficial/CodingTracker/Resources/doc/images/description.png new file mode 100644 index 000000000..b0107f4a5 Binary files /dev/null and b/CodingTracker.Endofficial/CodingTracker/Resources/doc/images/description.png differ diff --git a/CodingTracker.Endofficial/CodingTracker/Resources/doc/images/endTime.png b/CodingTracker.Endofficial/CodingTracker/Resources/doc/images/endTime.png new file mode 100644 index 000000000..7fd0342e4 Binary files /dev/null and b/CodingTracker.Endofficial/CodingTracker/Resources/doc/images/endTime.png differ diff --git a/CodingTracker.Endofficial/CodingTracker/Resources/doc/images/live-session.png b/CodingTracker.Endofficial/CodingTracker/Resources/doc/images/live-session.png new file mode 100644 index 000000000..0e7619efc Binary files /dev/null and b/CodingTracker.Endofficial/CodingTracker/Resources/doc/images/live-session.png differ diff --git a/CodingTracker.Endofficial/CodingTracker/Resources/doc/images/orderToDay.png b/CodingTracker.Endofficial/CodingTracker/Resources/doc/images/orderToDay.png new file mode 100644 index 000000000..c2a53a384 Binary files /dev/null and b/CodingTracker.Endofficial/CodingTracker/Resources/doc/images/orderToDay.png differ diff --git a/CodingTracker.Endofficial/CodingTracker/Resources/doc/images/orderToMonth.png b/CodingTracker.Endofficial/CodingTracker/Resources/doc/images/orderToMonth.png new file mode 100644 index 000000000..994d23764 Binary files /dev/null and b/CodingTracker.Endofficial/CodingTracker/Resources/doc/images/orderToMonth.png differ diff --git a/CodingTracker.Endofficial/CodingTracker/Resources/doc/images/orderToYear.png b/CodingTracker.Endofficial/CodingTracker/Resources/doc/images/orderToYear.png new file mode 100644 index 000000000..0a47f5d99 Binary files /dev/null and b/CodingTracker.Endofficial/CodingTracker/Resources/doc/images/orderToYear.png differ diff --git a/CodingTracker.Endofficial/CodingTracker/Resources/doc/images/play-session.png b/CodingTracker.Endofficial/CodingTracker/Resources/doc/images/play-session.png new file mode 100644 index 000000000..0c7e1ba66 Binary files /dev/null and b/CodingTracker.Endofficial/CodingTracker/Resources/doc/images/play-session.png differ diff --git a/CodingTracker.Endofficial/CodingTracker/Resources/doc/images/startTime.png b/CodingTracker.Endofficial/CodingTracker/Resources/doc/images/startTime.png new file mode 100644 index 000000000..044b8b319 Binary files /dev/null and b/CodingTracker.Endofficial/CodingTracker/Resources/doc/images/startTime.png differ diff --git a/CodingTracker.Endofficial/CodingTracker/Resources/doc/images/update.png b/CodingTracker.Endofficial/CodingTracker/Resources/doc/images/update.png new file mode 100644 index 000000000..16eaefc3e Binary files /dev/null and b/CodingTracker.Endofficial/CodingTracker/Resources/doc/images/update.png differ diff --git a/CodingTracker.Endofficial/CodingTracker/UserInterface.cs b/CodingTracker.Endofficial/CodingTracker/UserInterface.cs new file mode 100644 index 000000000..c879fc1d5 --- /dev/null +++ b/CodingTracker.Endofficial/CodingTracker/UserInterface.cs @@ -0,0 +1,60 @@ +using CodingTracker.Controller; +using Spectre.Console; +using static CodingTracker.Enums; + +namespace CodingTracker; + +internal class UserInterface +{ + internal void MainMenu() + { + bool closeApp = false; + + while (!closeApp) + { + AnsiConsole.Clear(); + var actionChoice = AnsiConsole.Prompt( + new SelectionPrompt() + .Title("What do you want to do next?") + .UseConverter(option => option switch // UseConverter is used to convert the enum values to user-friendly strings in the selection prompt + { + MenuAction.LiveSession => "Start a live coding session", + MenuAction.RegisterSession => "Register a coding session", + MenuAction.UpdateSession => "Update a coding session", + MenuAction.DeleteSession => "Delete a coding session", + MenuAction.ViewSessions => "View coding sessions", + MenuAction.ExiSession => "[red]Close App[/]", + _ => option.ToString() // Fallback to the default enum name if no specific string is provided + }) + .AddChoices(Enum.GetValues())); + + switch (actionChoice) + { + case MenuAction.LiveSession: + CodingController.LiveSession(); + break; + case MenuAction.RegisterSession: + CodingController.RegisterSession(); + break; + case MenuAction.UpdateSession: + CodingController.UpdateSession(); + break; + case MenuAction.DeleteSession: + CodingController.DeleteSession(); + break; + case MenuAction.ViewSessions: + CodingController.ViewSessions(); + break; + case MenuAction.ExiSession: + AnsiConsole.Status() + .Spinner(Spinner.Known.Dots) + .Start("Data rescue...", ctx => + { + Thread.Sleep(1000); + }); + closeApp = true; + break; + } + } + } +} diff --git a/CodingTracker.Endofficial/CodingTracker/appsettings.json b/CodingTracker.Endofficial/CodingTracker/appsettings.json new file mode 100644 index 000000000..e048c8bef --- /dev/null +++ b/CodingTracker.Endofficial/CodingTracker/appsettings.json @@ -0,0 +1,12 @@ +{ + "ConnectionStrings": { + "DefaultConnection": "Data Source=CodingTracker.db;" + }, + "FilePaths": { + "DatabaseDirectory": "D:/Programmazione/C#/C# Accademy/CodingTracker" + }, + "AppSettings": { + "ApplicationName": "Coding Tracker", + "Version": "1.0.0" + } +} \ No newline at end of file diff --git a/CodingTracker.Endofficial/UnitTesting.UnitTests/UnitTesting.UnitTests.csproj b/CodingTracker.Endofficial/UnitTesting.UnitTests/UnitTesting.UnitTests.csproj new file mode 100644 index 000000000..3cd134cc0 --- /dev/null +++ b/CodingTracker.Endofficial/UnitTesting.UnitTests/UnitTesting.UnitTests.csproj @@ -0,0 +1,22 @@ + + + + net9.0 + latest + enable + enable + false + + + + + + + + + + + + + + diff --git a/CodingTracker.Endofficial/UnitTesting.UnitTests/ValidatorInputTests.cs b/CodingTracker.Endofficial/UnitTesting.UnitTests/ValidatorInputTests.cs new file mode 100644 index 000000000..87af0939b --- /dev/null +++ b/CodingTracker.Endofficial/UnitTesting.UnitTests/ValidatorInputTests.cs @@ -0,0 +1,96 @@ +using CodingTracker.Controller; +using FluentAssertions; +using Xunit; +using Spectre.Console; +using Spectre.Console.Testing; +using System.Security.Cryptography.X509Certificates; + +namespace UnitTesting.UnitTests +{ + public class ValidatorInputTests + { + [Theory] + [InlineData("2020-11-25", "2020-11-25")] + [InlineData("0", "0")] + [InlineData(" 2020-11-25", "2020-11-25")] + [InlineData("2020-11-25 ", "2020-11-25")] + [InlineData("invalid\r2025-01-01", "2025-01-01")] // Wtih spectre.console don't use => "/n" + [InlineData("invalid\rinvalid\r2020-11-25", "2020-11-25")] + public void CorrectDateInput_ReturnCorrectDate(string inputDate, string expected) + { + // Arrange + var console = new TestConsole(); + console.Input.PushTextWithEnter(inputDate); // Simulates the user typing the sequence and pressing ENTER + + // Act + var result = InputInsert.GetDateSessionInput(console); + + // Assert + result.Should().NotBeNull(); + result.Should().Be(expected); + } + + [Theory] + [InlineData("Programming", "21:00", "22:00", 1, "21:00", "22:00")] + [InlineData("Programming", "20:00", "22:00", 2, "20:00", "22:00")] + [InlineData("Programming", "invalid\r21:00", "invalid\r22:00", 1, "21:00", "22:00")] + [InlineData("Programming", " 21:00", " 22:00", 1, "21:00", "22:00")] + [InlineData("Programming", "21:00 ", "22:00 ", 1, "21:00", "22:00")] + public void CorrectTimeSessionInput_ReturnCorrectCodingSession(string description, string start, string end, int durationexpected, string startExp, string endExp) + { + // Arrange + var console = new TestConsole(); + console.Input.PushTextWithEnter(description); + console.Input.PushTextWithEnter(start); + console.Input.PushTextWithEnter(end); + console.Input.PushTextWithEnter(" "); + + // Act + var result = InputInsert.GetTimeSessionInput("2026-12-25", console); + + // Assert + result.Should().NotBeNull(); + result.Description.Should().Be(description); + result.StartTime.ToString("HH:mm").Should().Be(startExp); + result.EndTime.ToString("HH:mm").Should().Be(endExp); + result.Date.Should().Be("2026-12-25"); + result.Duration.TotalHours.Should().Be(durationexpected); + } + + [Theory] + [InlineData("3", 3)] + [InlineData("invalid\r4", 4)] + [InlineData(" 4", 4)] + [InlineData("4 ", 4)] + [InlineData("0", 0)] + public void ReturnCorrectId(string inputId, int expected) + { + // Arrange + var console = new TestConsole(); + console.Input.PushTextWithEnter(inputId); + + // Act + var result = InputInsert.GetId(console); + + // Assert + result.Should().Be(expected); + + } + + [Theory] + [InlineData("Working", "Working")] + public void ReturCorrectOnlyDescription(string inputDescription, string expected) + { + // Arrange + var console = new TestConsole(); + console.Input.PushTextWithEnter(inputDescription); + + // Act + var result = InputInsert.OnlyDescription(console); + + // Assert + result.Should().Be(expected); + } + } + +}