diff --git a/.gitattributes b/.gitattributes new file mode 100644 index 000000000..1ff0c4230 --- /dev/null +++ b/.gitattributes @@ -0,0 +1,63 @@ +############################################################################### +# Set default behavior to automatically normalize line endings. +############################################################################### +* text=auto + +############################################################################### +# Set default behavior for command prompt diff. +# +# This is need for earlier builds of msysgit that does not have it on by +# default for csharp files. +# Note: This is only used by command line +############################################################################### +#*.cs diff=csharp + +############################################################################### +# Set the merge driver for project and solution files +# +# Merging from the command prompt will add diff markers to the files if there +# are conflicts (Merging from VS is not affected by the settings below, in VS +# the diff markers are never inserted). Diff markers may cause the following +# file extensions to fail to load in VS. An alternative would be to treat +# these files as binary and thus will always conflict and require user +# intervention with every merge. To do so, just uncomment the entries below +############################################################################### +#*.sln merge=binary +#*.csproj merge=binary +#*.vbproj merge=binary +#*.vcxproj merge=binary +#*.vcproj merge=binary +#*.dbproj merge=binary +#*.fsproj merge=binary +#*.lsproj merge=binary +#*.wixproj merge=binary +#*.modelproj merge=binary +#*.sqlproj merge=binary +#*.wwaproj merge=binary + +############################################################################### +# behavior for image files +# +# image files are treated as binary by default. +############################################################################### +#*.jpg binary +#*.png binary +#*.gif binary + +############################################################################### +# diff behavior for common document formats +# +# Convert binary document formats to text before diffing them. This feature +# is only available from the command line. Turn it on by uncommenting the +# entries below. +############################################################################### +#*.doc diff=astextplain +#*.DOC diff=astextplain +#*.docx diff=astextplain +#*.DOCX diff=astextplain +#*.dot diff=astextplain +#*.DOT diff=astextplain +#*.pdf diff=astextplain +#*.PDF diff=astextplain +#*.rtf diff=astextplain +#*.RTF diff=astextplain diff --git a/CodingTracker.UnitTests/CodingSessionRepositoryTests.cs b/CodingTracker.UnitTests/CodingSessionRepositoryTests.cs new file mode 100644 index 000000000..1241e29ff --- /dev/null +++ b/CodingTracker.UnitTests/CodingSessionRepositoryTests.cs @@ -0,0 +1,46 @@ +using Microsoft.Extensions.Configuration; +using System.Globalization; + +namespace CodingTracker.UnitTests +{ + public class CodingSessionRepositoryTests + { + [SetUp] + public void Setup() + { + var config = new ConfigurationBuilder() + .SetBasePath(AppContext.BaseDirectory) + .AddJsonFile("appsettings.json", optional: false) + .Build(); + + DatabaseManager.SetConfiguration(config); + DatabaseManager.Start(); + DatabaseManager.EmptyDb(); + } + + [TestCase("Coding Tracker", "C#", 2026, 9, 21, 18, 30, 0, 2026, 9, 21, 20, 15, 0)] + [TestCase("Videogame", "C++", 2026, 9, 22, 10, 0, 0, 2026, 9, 22, 12, 45, 0)] + public void CRUD_CodingSessions(string project, string language, int year1, int month1, int day1, int hours1, int minutes1, int seconds1, int year2, int month2, int day2, int hours2, int minutes2, int seconds2) + { + List newSessions = new() + { + new() { Project = project, Language = language, + StartTime = new DateTime(year1, month1, day1, hours1, minutes1, seconds1), + EndTime = new DateTime(year2, month2, day2, hours2, minutes2, seconds2) }, + }; + + CodingSessionRepository.InsertSessions(newSessions); + var sessions = CodingSessionRepository.GetSessions(); + Assert.That(sessions, Is.Not.Empty); + + foreach(var session in sessions) + { + int rowsDeleted = CodingSessionRepository.DeleteSession(session.Id); + Assert.That(rowsDeleted, Is.EqualTo(1)); + } + + var empty = CodingSessionRepository.GetSessions(); + Assert.That(empty, Is.Empty); + } + } +} diff --git a/CodingTracker.UnitTests/CodingTracker.UnitTests.csproj b/CodingTracker.UnitTests/CodingTracker.UnitTests.csproj new file mode 100644 index 000000000..3008e1fd8 --- /dev/null +++ b/CodingTracker.UnitTests/CodingTracker.UnitTests.csproj @@ -0,0 +1,27 @@ + + + + net10.0 + latest + enable + enable + false + + + + + + + + + + + + + + + + + + + diff --git a/CodingTracker.UnitTests/InputValidationTests.cs b/CodingTracker.UnitTests/InputValidationTests.cs new file mode 100644 index 000000000..383d8651e --- /dev/null +++ b/CodingTracker.UnitTests/InputValidationTests.cs @@ -0,0 +1,38 @@ +using System.Globalization; + +namespace CodingTracker.UnitTests +{ + public class InputValidationTests + { + private static readonly object[] ValidDateTimeCases = + { + new TestCaseData("2026-12-12 10:05:13", true), + new TestCaseData("2025-13-12 20:12:26", false), + new TestCaseData("2021-10-44 01:02:03", false), + new TestCaseData("2021-10-12 60:70:80", false), + + new TestCaseData("2021-10-12", false), + new TestCaseData("", false), + new TestCaseData("asdf", false), + new TestCaseData(null, false) + }; + + [TestCaseSource(nameof(ValidDateTimeCases))] + public void ValidDateTime_ReturnsTrue(string inputDate, bool expectedResult) + { + bool isValidDate = InputValidation.ValidateDateTime(inputDate, out DateTime dateValidated); + Assert.That(isValidDate, Is.EqualTo(expectedResult)); + } + + [TestCase("2026-12-12 10:05:13", "2026-12-12 10:05:14", true)] + [TestCase("2026-12-12 10:05:13", "2026-12-12 10:05:13", true)] + [TestCase("2026-12-12 10:05:13", "2026-12-12 10:05:12", false)] + public void ValidEndTime_ReturnsTrue(string startDate, string endDate, bool expectedResult) + { + DateTime start = DateTime.ParseExact(startDate, "yyyy-MM-dd HH:mm:ss", CultureInfo.InvariantCulture, DateTimeStyles.None); + DateTime end = DateTime.ParseExact(endDate, "yyyy-MM-dd HH:mm:ss", CultureInfo.InvariantCulture, DateTimeStyles.None); + bool isValidDate = InputValidation.ValidateEndTime(start, end); + Assert.That(isValidDate, Is.EqualTo(expectedResult)); + } + } +} diff --git a/CodingTracker.UnitTests/Properties/launchSettings.json b/CodingTracker.UnitTests/Properties/launchSettings.json new file mode 100644 index 000000000..d780df0e7 --- /dev/null +++ b/CodingTracker.UnitTests/Properties/launchSettings.json @@ -0,0 +1,8 @@ +{ + "profiles": { + "CodingTracker.UnitTests": { + "commandName": "Project", + "workingDirectory": "$(ProjectDir)" + } + } +} \ No newline at end of file diff --git a/CodingTracker.slnx b/CodingTracker.slnx new file mode 100644 index 000000000..c472ca768 --- /dev/null +++ b/CodingTracker.slnx @@ -0,0 +1,4 @@ + + + + diff --git a/CodingTracker/CodingSession.cs b/CodingTracker/CodingSession.cs new file mode 100644 index 000000000..d688342bb --- /dev/null +++ b/CodingTracker/CodingSession.cs @@ -0,0 +1,25 @@ +namespace CodingTracker +{ + public class CodingSession + { + public int Id { get; set; } + public string Project { get; set; } = ""; + public string Language { get; set; } = ""; + public DateTime StartTime { get; set; } + public DateTime? EndTime { get; set; } + public TimeSpan Duration => EndTime == null ? DateTime.Now - StartTime : EndTime.Value - StartTime; + + public CodingSession() + { + + } + + public CodingSession(string project, string language, DateTime startTime, DateTime? endTime) + { + Project = project; + Language = language; + StartTime = startTime; + EndTime = endTime; + } + } +} diff --git a/CodingTracker/CodingSessionRepository.cs b/CodingTracker/CodingSessionRepository.cs new file mode 100644 index 000000000..03267f5e1 --- /dev/null +++ b/CodingTracker/CodingSessionRepository.cs @@ -0,0 +1,35 @@ +using Dapper; +using Microsoft.Data.Sqlite; + +namespace CodingTracker +{ + public static class CodingSessionRepository + { + public static List GetSessions() + { + using SqliteConnection connection = DatabaseManager.CreateConnection(); + var sessions = connection.Query("SELECT * FROM CodingSessions"); + return sessions.ToList(); + } + + public static int InsertSessions(List sessions) + { + using SqliteConnection connection = DatabaseManager.CreateConnection(); + connection.Open(); + using SqliteTransaction transaction = connection.BeginTransaction(); + + string query = "INSERT INTO CodingSessions (Project, Language, StartTime, EndTime) VALUES (@Project, @Language, @StartTime, @EndTime)"; + int rowsInserted = connection.Execute(query, sessions, transaction); + + transaction.Commit(); + return rowsInserted; + } + + public static int DeleteSession(int id) + { + using SqliteConnection connection = DatabaseManager.CreateConnection(); + string query = "DELETE FROM CodingSessions WHERE Id = @Id"; + return connection.Execute(query, new { Id = id }); + } + } +} diff --git a/CodingTracker/CodingTracker.csproj b/CodingTracker/CodingTracker.csproj new file mode 100644 index 000000000..899a8f419 --- /dev/null +++ b/CodingTracker/CodingTracker.csproj @@ -0,0 +1,25 @@ + + + + Exe + net10.0 + enable + enable + + + + + + + + + + + + PreserveNewest + + + + + + diff --git a/CodingTracker/DatabaseManager.cs b/CodingTracker/DatabaseManager.cs new file mode 100644 index 000000000..b542f1f7d --- /dev/null +++ b/CodingTracker/DatabaseManager.cs @@ -0,0 +1,74 @@ +using Dapper; +using Microsoft.Data.Sqlite; +using Microsoft.Extensions.Configuration; + +namespace CodingTracker +{ + public static class DatabaseManager + { + private static IConfiguration _config = null!; + private static string _connectionString = ""; + + public static void SetConfiguration(IConfiguration config) + { + _config = config; + _connectionString = _config.GetConnectionString("DefaultConnection"); + } + + internal static SqliteConnection CreateConnection() + { + return new(_connectionString); + } + + public static void Start() + { + using SqliteConnection connection = CreateConnection(); + string query = + """ + CREATE TABLE IF NOT EXISTS CodingSessions ( + Id INTEGER PRIMARY KEY AUTOINCREMENT, + Project TEXT, + Language TEXT, + StartTime TEXT, + EndTime TEXT + ) + """; + connection.Execute(query); + } + + /// + /// Inserts some default values for the user to view + /// + internal static void Initialize() + { + if (IsEmpty()) + { + List sessions = new() + { + new(".NET Course", "C#", new DateTime(2026, 9, 22, 18, 30, 0), null), + new() { Project = "Coding Tracker", Language = "C#", + StartTime = new DateTime(2026, 9, 21, 18, 30, 0), + EndTime = new DateTime(2026, 9, 21, 20, 15, 0) }, + new() { Project = "Platformer", Language = "C++", + StartTime = new DateTime(2026, 9, 22, 10, 0, 0), + EndTime = new DateTime(2026, 9, 22, 12, 45, 0) } + }; + CodingSessionRepository.InsertSessions(sessions); + } + } + + private static bool IsEmpty() + { + using SqliteConnection connection = CreateConnection(); + int count = connection.ExecuteScalar("SELECT EXISTS (SELECT 1 FROM CodingSessions)"); + return count == 0; + } + + public static void EmptyDb() + { + using SqliteConnection connection = CreateConnection(); + string query = "DELETE FROM CodingSessions"; + connection.Execute(query); + } + } +} diff --git a/CodingTracker/InputValidation.cs b/CodingTracker/InputValidation.cs new file mode 100644 index 000000000..3c925e944 --- /dev/null +++ b/CodingTracker/InputValidation.cs @@ -0,0 +1,32 @@ +using System.Globalization; + +namespace CodingTracker +{ + public static class InputValidation + { + /// + /// Checks if date is valid + /// + /// + /// + public static bool ValidateDateTime(string dateInput, out DateTime dateValidated) + { + if (dateInput == null) + { + dateValidated = default; + return false; + } + bool validDate = false; + if (DateTime.TryParseExact(dateInput, "yyyy-MM-dd HH:mm:ss", CultureInfo.InvariantCulture, DateTimeStyles.None, out dateValidated)) + { + validDate = true; + } + return validDate; + } + + public static bool ValidateEndTime(DateTime startTime, DateTime endTime) + { + return (endTime - startTime).TotalSeconds >= 0; + } + } +} diff --git a/CodingTracker/Program.cs b/CodingTracker/Program.cs new file mode 100644 index 000000000..32b5666e2 --- /dev/null +++ b/CodingTracker/Program.cs @@ -0,0 +1,20 @@ +using CodingTracker; +using Microsoft.Extensions.Configuration; + +class Program +{ + static void Main(string[] args) + { + var config = new ConfigurationBuilder() + .SetBasePath(AppContext.BaseDirectory) + .AddJsonFile("appsettings.json", optional: false) + .Build(); + + DatabaseManager.SetConfiguration(config); + DatabaseManager.Start(); + DatabaseManager.Initialize(); + + UIController ui = new(config); + ui.MainMenu(); + } +} \ No newline at end of file diff --git a/CodingTracker/Properties/launchSettings.json b/CodingTracker/Properties/launchSettings.json new file mode 100644 index 000000000..4391c4353 --- /dev/null +++ b/CodingTracker/Properties/launchSettings.json @@ -0,0 +1,8 @@ +{ + "profiles": { + "CodingTracker": { + "commandName": "Project", + "workingDirectory": "$(ProjectDir)" + } + } +} \ No newline at end of file diff --git a/CodingTracker/TableVisualizationEngine.cs b/CodingTracker/TableVisualizationEngine.cs new file mode 100644 index 000000000..7f3bdee1a --- /dev/null +++ b/CodingTracker/TableVisualizationEngine.cs @@ -0,0 +1,18 @@ +using Spectre.Console; + +namespace CodingTracker +{ + internal static class TableVisualizationEngine + { + internal static Table CreateTable(List sessions) + { + Table table = new Table().AddColumns("Id", "Project", "Language", "Start time", "End time", "Duration"); + foreach (CodingSession session in sessions) + { + var duration = session.Duration; + table.AddRow(session.Id.ToString(), session.Project, session.Language, session.StartTime.ToString(), session.EndTime?.ToString() ?? "In progress", $"{(int)duration.TotalHours}h {duration.Minutes}m {duration.Seconds}s"); + } + return table; + } + } +} diff --git a/CodingTracker/UIController.cs b/CodingTracker/UIController.cs new file mode 100644 index 000000000..fa468f6aa --- /dev/null +++ b/CodingTracker/UIController.cs @@ -0,0 +1,442 @@ +using Microsoft.Extensions.Configuration; +using Spectre.Console; +using System.Globalization; +using System.Runtime.Serialization; + +namespace CodingTracker +{ + internal class UIController + { + internal enum MenuOption + { + TrackSession, + ViewSessions, + AddSession, + DeleteSession, + ExitApplication + } + + internal enum SortOrder + { + Ascending, + Descending + } + + private Dictionary options = new() + { + ["Track Session"] = MenuOption.TrackSession, + ["View Sessions"] = MenuOption.ViewSessions, + ["Add Sessions"] = MenuOption.AddSession, + ["Delete Sessions"] = MenuOption.DeleteSession, + ["Exit Application"] = MenuOption.ExitApplication + }; + + private static IConfiguration _config = null!; + private static string DateFormat = null!; + + private static CodingSession? sessionInProgress = null; + + private static DateTime Now() => + DateTime.Now.AddTicks(-(DateTime.Now.Ticks % TimeSpan.TicksPerSecond)); + + public UIController(IConfiguration config) + { + _config = config; + DateFormat = _config["DateTimeFormat"]; + } + + internal void MainMenu() + { + bool exitApp = false; + while (!exitApp) + { + AnsiConsole.Clear(); + MenuOption option = SelectMenuOption(); + switch (option) + { + case MenuOption.TrackSession: + TrackSession(); + break; + case MenuOption.ViewSessions: + ViewSessions(); + break; + case MenuOption.AddSession: + AddSession(); + break; + case MenuOption.DeleteSession: + DeleteSession(); + break; + case MenuOption.ExitApplication: + exitApp = true; + AnsiConsole.Clear(); + break; + default: + break; + } + } + } + + private MenuOption SelectMenuOption() + { + bool optionSelected = false; + MenuOption option = default; + while (!optionSelected) + { + string selected = AnsiConsole.Prompt( + new SelectionPrompt() + .HighlightStyle(Style.Parse("cyan")) + .Title("Main Menu") + .AddChoices(options.Keys) + ); + optionSelected = options.TryGetValue(selected, out option); + } + return option; + } + + private void TrackSession() + { + AnsiConsole.WriteLine("Stopwatch for tracking session in progress."); + + if (sessionInProgress == null) + { + string project = AnsiConsole.Ask("Write the name of the project you are coding:"); + string language = AnsiConsole.Ask("Write the language you are coding in:"); + string startAction = AnsiConsole.Prompt( + new SelectionPrompt() + .HighlightStyle(Style.Parse("cyan")) + .AddChoices("Start", "Cancel") + ); + if (startAction != "Start") return; + sessionInProgress = new(project, language, Now(), null); + } + + AnsiConsole.Clear(); + AnsiConsole.Write(TableVisualizationEngine.CreateTable([sessionInProgress])); + string stopAction = AnsiConsole.Prompt( + new SelectionPrompt() + .HighlightStyle(Style.Parse("cyan")) + .AddChoices("Go Back", "Stop time") + ); + + if(stopAction == "Stop time") + { + AnsiConsole.Clear(); + sessionInProgress.EndTime = Now(); + InsertSession(sessionInProgress); + } + } + + private void ViewSessions() + { + var sessions = CodingSessionRepository.GetSessions(); + if(sessionInProgress != null) + { + sessions.Add(sessionInProgress); + } + + string viewChoice = AnsiConsole.Prompt( + new SelectionPrompt() + .HighlightStyle(Style.Parse("cyan")) + .AddChoices("View all", "View filtered") + ); + if(viewChoice == "View filtered") + { + sessions = FilterSessions(sessions); + } + sessions = SortSessions(sessions); + + Table table = TableVisualizationEngine.CreateTable(sessions); + Panel menu = new Panel(table).Header("Coding Sessions").Expand(); + menu.Height = Console.WindowHeight - 1; + AnsiConsole.Write(menu); + string action = AnsiConsole.Prompt( + new SelectionPrompt() + .HighlightStyle(Style.Parse("cyan")) + .AddChoices("Go back") + ); + } + + private List FilterSessions(List sessions) + { + List filterChoices = AnsiConsole.Prompt( + new MultiSelectionPrompt() + .HighlightStyle(Style.Parse("cyan")) + .Title("Filter by (space to select, enter to confirm)") + .AddChoices("Time period", "Project", "Language", "Min duration") + ); + + foreach(string choice in filterChoices) + { + switch(choice) + { + case "Time period": + sessions = FilterByPeriod(sessions); + break; + case "Language": + sessions = FilterByLanguage(sessions); + break; + case "Project": + sessions = FilterByProject(sessions); + break; + case "Min duration": + sessions = FilterByMinDuration(sessions); + break; + default: + break; + } + } + return sessions; + } + + private List SortSessions(List sessions) + { + string sortChoice = AnsiConsole.Prompt( + new SelectionPrompt() + .HighlightStyle(Style.Parse("cyan")) + .Title("Sort by") + .AddChoices("Id", "Project", "Language", "Start time", "End time", "Duration") + ); + switch (sortChoice) + { + case "Id": + sessions.Sort((a, b) => a.Id.CompareTo(b.Id)); + break; + case "Language": + sessions.Sort((a, b) => a.Language.CompareTo(b.Language)); + break; + case "Project": + sessions.Sort((a, b) => a.Project.CompareTo(b.Project)); + break; + case "Start time": + sessions.Sort((a, b) => a.StartTime.CompareTo(b.StartTime)); + break; + case "End time": + sessions.Sort((a, b) => + { + if (a.EndTime == null && b.EndTime == null) return 0; + if (a.EndTime == null) return 1; + if (b.EndTime == null) return -1; + return a.EndTime.Value.CompareTo(b.EndTime.Value); + }); + break; + case "Duration": + sessions.Sort((a, b) => a.Duration.CompareTo(b.Duration)); + break; + } + + SortOrder sortOrder = AnsiConsole.Prompt( + new SelectionPrompt() + .HighlightStyle(Style.Parse("cyan")) + .Title("Sort order") + .AddChoices(SortOrder.Ascending, SortOrder.Descending) + ); + if (sortOrder == SortOrder.Descending) sessions.Reverse(); + + return sessions; + } + + private List FilterByPeriod(List sessions) + { + List result = new(); + var (start, end) = PromptPeriod(); + foreach (CodingSession session in sessions) + { + if (session.StartTime >= start && session.StartTime < end) + { + result.Add(session); + } + } + return result; + } + + private (DateTime, DateTime) PromptPeriod() + { + string period = AnsiConsole.Prompt( + new SelectionPrompt() + .HighlightStyle(Style.Parse("cyan")) + .Title("Period") + .AddChoices("Today", "This week", "This month", "This year", "Custom range") + ); + DateTime today = DateTime.Today; + (DateTime, DateTime) datePeriod = new(); + + switch(period) + { + case "Today": + datePeriod = (today, today.AddDays(1)); + break; + case "This week": + datePeriod = (today.AddDays(-(int)today.DayOfWeek), today.AddDays(7 - (int)today.DayOfWeek)); + break; + case "This month": + datePeriod = (new DateTime(today.Year, today.Month, 1), new DateTime(today.Year, today.Month, 1).AddMonths(1)); + break; + case "This year": + datePeriod = (new DateTime(today.Year, 1, 1), new DateTime(today.Year + 1, 1, 1)); + break; + case "Custom range": + datePeriod = PromptCustomRange(); + break; + } + return datePeriod; + } + + private (DateTime, DateTime) PromptCustomRange() + { + DateTime startTime = default; + DateTime endTime = default; + bool validDate = false; + while (!validDate) + { + string startTimeInput = AnsiConsole.Ask("Write the start date in (yyyy-MM-dd) format:"); + validDate = InputValidation.ValidateDateTime(startTimeInput, out startTime); + } + validDate = false; + while (!validDate) + { + string endTimeInput = AnsiConsole.Ask("Write the end date in (yyyy-MM-dd) format:"); + validDate = InputValidation.ValidateDateTime(endTimeInput, out endTime); + validDate = InputValidation.ValidateEndTime(startTime, endTime); + endTime.AddDays(1); + } + return (startTime, endTime); + } + + private List FilterByLanguage(List sessions) + { + List result = new(); + string language = AnsiConsole.Ask("Language:"); + foreach (CodingSession session in sessions) + { + if(session.Language.Equals(language, StringComparison.OrdinalIgnoreCase)) + { + result.Add(session); + } + } + return result; + } + + private List FilterByProject(List sessions) + { + List result = new(); + string project = AnsiConsole.Ask("Project:"); + foreach (CodingSession session in sessions) + { + if (session.Project.Equals(project, StringComparison.OrdinalIgnoreCase)) + { + result.Add(session); + } + } + return result; + } + + private List FilterByMinDuration(List sessions) + { + List result = new(); + int minutes = AnsiConsole.Ask("Minimum duration (minutes):"); + foreach (CodingSession session in sessions) + { + if(session.Duration.TotalMinutes >= minutes) + { + result.Add(session); + } + } + return result; + } + + private void AddSession() + { + string project = AnsiConsole.Ask("Write the name of the project you are coding:"); + string language = AnsiConsole.Ask("Write the language you are coding in:"); + DateTime startTime = default; + DateTime endTime = default; + bool validDate = false; + while (!validDate) + { + string startTimeInput = AnsiConsole.Ask("Write the start time in (yyyy-MM-dd HH:mm:ss) format:"); + validDate = InputValidation.ValidateDateTime(startTimeInput, out startTime); + } + validDate = false; + while (!validDate) + { + string endTimeInput = AnsiConsole.Ask("Write the end time in (yyyy-MM-dd HH:mm:ss) format:"); + validDate = InputValidation.ValidateDateTime(endTimeInput, out endTime); + validDate = InputValidation.ValidateEndTime(startTime, endTime); + } + CodingSession newSession = new(project, language, startTime, endTime); + InsertSession(newSession); + } + + private void InsertSession(CodingSession newSession) + { + AnsiConsole.Write(TableVisualizationEngine.CreateTable([newSession])); + bool insertNew = AnsiConsole.Confirm($"Do you want to add this new session?"); + if (insertNew) + { + int rowsInserted = CodingSessionRepository.InsertSessions([newSession]); + AnsiConsole.MarkupLine($"[{(rowsInserted > 0 ? "lime" : "red")}]{rowsInserted} new sessions added[/]"); + string action = AnsiConsole.Prompt( + new SelectionPrompt() + .HighlightStyle(Style.Parse("cyan")) + .AddChoices("Go back") + ); + } + } + + private void DeleteSession() + { + var sessions = CodingSessionRepository.GetSessions(); + bool keepDeleting = true; + while (keepDeleting) + { + if (sessions.Count() == 0) + { + AnsiConsole.WriteLine("There are no sessions to delete"); + string action = AnsiConsole.Prompt( + new SelectionPrompt() + .HighlightStyle(Style.Parse("cyan")) + .AddChoices("Go back") + ); + return; + } + + AnsiConsole.WriteLine("Select a session to delete\n"); + + CodingSession? selected = PromptSelectSession(sessions); + if (selected == null) return; + + AnsiConsole.Write(TableVisualizationEngine.CreateTable([selected])); + bool delete = AnsiConsole.Confirm("Are you sure you want to delete this session entry?"); + if (!delete) + { + AnsiConsole.Clear(); + continue; + } + sessions.Remove(selected); + int rowsDeleted = CodingSessionRepository.DeleteSession(selected.Id); + AnsiConsole.WriteLine($"{rowsDeleted} row deleted: {selected.Id} "); + keepDeleting = AnsiConsole.Confirm("Do you want to delete more sessions?"); + } + } + + private static CodingSession? PromptSelectSession(List sessions) + { + CodingSession cancelOption = new(){ Id = -1 }; + if (!sessions.Exists(s => s.Id == cancelOption.Id)) + { + sessions.Add(cancelOption); + } + CodingSession selected = AnsiConsole.Prompt( + new SelectionPrompt() + .HighlightStyle(Style.Parse("cyan")) + .Title($" {"Id",-4} {"Project",-15} {"Language",-10} {"Start",-20} {"End",-20} {"Duration",-11}") + .UseConverter( + s => s.Id == -1 + ? "[grey]Cancel[/]" + : $"{s.Id,-4} {s.Project,-15} {s.Language,-10} {s.StartTime.ToString(DateFormat).PadRight(19)} {s.EndTime?.ToString(DateFormat).PadRight(19) ?? "In progress".PadRight(19)} {(int)s.Duration.TotalHours,2}h {s.Duration.Minutes:00}m {s.Duration.Seconds:00}s") + .AddChoices(sessions) + ); + return selected.Id == -1 ? null : selected; + } + } +} diff --git a/CodingTracker/appsettings.json b/CodingTracker/appsettings.json new file mode 100644 index 000000000..242d7c124 --- /dev/null +++ b/CodingTracker/appsettings.json @@ -0,0 +1,6 @@ +{ + "ConnectionStrings": { + "DefaultConnection": "Data Source=coding-tracker.db;Foreign Keys=true" + }, + "DateTimeFormat": "yyyy-MM-dd HH:mm:ss" +} \ No newline at end of file diff --git a/README.md b/README.md new file mode 100644 index 000000000..0448ddf10 --- /dev/null +++ b/README.md @@ -0,0 +1,47 @@ +# Coding Tracker + +A simple console-based coding tracker. This is a project for the C# Academy course. +You can view your coding sessions, delete them and track a session as it happens. + +## How to use + +As this is a console application you just need to input the option you want and press enter. + +### Main Menu + +On starting the program you get presented all the options available. + +#### Track Session + +You can track a session in progress and stop the time when you are finished. + +#### View Sessions + +Shows all the sessions saved. You can filter by any field and order them. + +#### Add Sessions + +Add a session you've completed before by writing all fields. + +#### Delete Sessions + +Delete a session. + +#### Exit Application + +Close the program. + + +# My thoughts on this project + +## What I've learned + +- First time using Spectre Console. Very useful for simple console applications as it gives you almost anything you need. +- Also first time using Dapper. As this was a simpler project than the previous one I didn't need much from it so I hope to use it more in the future to learn in more in depth. +- How much not coding during a few months affects your ability. It cost me some effort to even remember what I was doing making this program. + +## Things to improve + +- As the project was small I've only made a few unit tests, but it's better than nothing I guess. +- Again not coding for so long made the architecture not the best. Even though I've known OOP for a long time I didn't make the best design decisions this time. +- I forgot to make good use of Git and only made like 2 commits, definitely need to remember for the next project. \ No newline at end of file