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.
+
+ -
+
+## Functionality & Usage
+
+- **Live session**
+ - ***Date recording***: Record date of the session.
+ -
+
+ - ***User choice***: Type 'P' or 'p' to continue. Type '0' to return to main menu.
+ -
+
+ - ***Description***: Add a description for the session.
+ -
+
+ - ***Start session***: The stopwatch is start and it is stop when the user presses any key.
+ -
+
+- **Register a new session**
+ - ***Date recording***: Record date of the session.
+ - ***Description***: Add a description for the session.
+ - ***Start time***: Enter the start time.
+ -
+
+ - ***End time***: Enter the end time.
+ -
+
+- **View sessions**
+ - ***View of the sessions***: It's shown a table with all the sessions.
+ -
+
+ - ***Fiters***: A list of filters is displayed
+ -
+
+ - ***Year filter***: The user can choose the year that he wants to view is displayed.
+ -
+
+ - ***Month filter***: The user can choose the month that he wants to view is displayed.
+ -
+
+ - ***Day filter***: The user can choose the day that he wants to view is displayed.
+ -
+
+ - ***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.
+
+ -
+
+- **Delete session**
+ - ***View of the sessions***: It's shown a table with all the sessions.
+ -
+
+ - ***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.
+ -
+
+## 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);
+ }
+ }
+
+}