From d22365fef81c577065b72c39ad1a02fc61c9d35b Mon Sep 17 00:00:00 2001 From: kilozdazolik Date: Mon, 8 Dec 2025 20:46:32 +0100 Subject: [PATCH 1/3] feat: finish project --- .../.idea/.gitignore | 15 ++ .../CodingTracker.Tests.csproj | 27 +++ .../ControllerTests/TrackerControllerTests.cs | 31 ++++ .../HelperTests/HelperTests.cs | 71 +++++++ .../ServiceTests/TrackerServiceTests.cs | 51 ++++++ .../CodingTracker.Tests.GlobalUsings.g.cs | 9 + .../CodingTracker.kilozdazolik.csproj | 24 +++ .../Controllers/TrackerController.cs | 173 ++++++++++++++++++ CodingTracker.kilozdazolik/Data/Database.cs | 82 +++++++++ CodingTracker.kilozdazolik/Enums/Enum.cs | 18 ++ CodingTracker.kilozdazolik/Helper.cs | 65 +++++++ .../Models/TrackerModel.cs | 9 + CodingTracker.kilozdazolik/Program.cs | 6 + .../Services/ITrackerService.cs | 12 ++ .../Services/TrackerService.cs | 116 ++++++++++++ CodingTracker.kilozdazolik/UserInterface.cs | 72 ++++++++ CodingTracker.kilozdazolik/appsettings.json | 5 + UnitTests.kilozdazolik.sln | 22 +++ 18 files changed, 808 insertions(+) create mode 100644 .idea/.idea.UnitTests.kilozdazolik/.idea/.gitignore create mode 100644 CodingTracker.Tests/CodingTracker.Tests.csproj create mode 100644 CodingTracker.Tests/ControllerTests/TrackerControllerTests.cs create mode 100644 CodingTracker.Tests/HelperTests/HelperTests.cs create mode 100644 CodingTracker.Tests/ServiceTests/TrackerServiceTests.cs create mode 100644 CodingTracker.Tests/obj/Debug/net10.0/CodingTracker.Tests.GlobalUsings.g.cs create mode 100644 CodingTracker.kilozdazolik/CodingTracker.kilozdazolik.csproj create mode 100644 CodingTracker.kilozdazolik/Controllers/TrackerController.cs create mode 100644 CodingTracker.kilozdazolik/Data/Database.cs create mode 100644 CodingTracker.kilozdazolik/Enums/Enum.cs create mode 100644 CodingTracker.kilozdazolik/Helper.cs create mode 100644 CodingTracker.kilozdazolik/Models/TrackerModel.cs create mode 100644 CodingTracker.kilozdazolik/Program.cs create mode 100644 CodingTracker.kilozdazolik/Services/ITrackerService.cs create mode 100644 CodingTracker.kilozdazolik/Services/TrackerService.cs create mode 100644 CodingTracker.kilozdazolik/UserInterface.cs create mode 100644 CodingTracker.kilozdazolik/appsettings.json create mode 100644 UnitTests.kilozdazolik.sln diff --git a/.idea/.idea.UnitTests.kilozdazolik/.idea/.gitignore b/.idea/.idea.UnitTests.kilozdazolik/.idea/.gitignore new file mode 100644 index 0000000..c8b60a7 --- /dev/null +++ b/.idea/.idea.UnitTests.kilozdazolik/.idea/.gitignore @@ -0,0 +1,15 @@ +# Default ignored files +/shelf/ +/workspace.xml +# Rider ignored files +/modules.xml +/.idea.UnitTests.kilozdazolik.iml +/projectSettingsUpdater.xml +/contentModel.xml +# Ignored default folder with query files +/queries/ +# Datasource local storage ignored files +/dataSources/ +/dataSources.local.xml +# Editor-based HTTP Client requests +/httpRequests/ diff --git a/CodingTracker.Tests/CodingTracker.Tests.csproj b/CodingTracker.Tests/CodingTracker.Tests.csproj new file mode 100644 index 0000000..171f487 --- /dev/null +++ b/CodingTracker.Tests/CodingTracker.Tests.csproj @@ -0,0 +1,27 @@ + + + + net10.0 + enable + enable + false + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/CodingTracker.Tests/ControllerTests/TrackerControllerTests.cs b/CodingTracker.Tests/ControllerTests/TrackerControllerTests.cs new file mode 100644 index 0000000..45a80bc --- /dev/null +++ b/CodingTracker.Tests/ControllerTests/TrackerControllerTests.cs @@ -0,0 +1,31 @@ +using CodingTracker.kilozdazolik.Controllers; +using CodingTracker.kilozdazolik.Models; +using CodingTracker.kilozdazolik.Services; +using FakeItEasy; + +namespace CodingTracker.Tests.ControllerTests; + +public class TrackerControllerTests +{ + private readonly ITrackerService _service; + private readonly TrackerController _controller; + + public TrackerControllerTests() + { + _service = A.Fake(); + _controller = new TrackerController(_service); + } + + [Fact] + public void ViewAllSessions_CallsServiceToGetAllSessions() + { + //Arrange + A.CallTo(() => _service.GetAllSession()).Returns(new List()); + + //Act + _controller.ViewAllSessions(); + + //Assert + A.CallTo(() => _service.GetAllSession()).MustHaveHappenedOnceExactly(); + } +} \ No newline at end of file diff --git a/CodingTracker.Tests/HelperTests/HelperTests.cs b/CodingTracker.Tests/HelperTests/HelperTests.cs new file mode 100644 index 0000000..4ba7664 --- /dev/null +++ b/CodingTracker.Tests/HelperTests/HelperTests.cs @@ -0,0 +1,71 @@ +using CodingTracker.kilozdazolik; +using FluentAssertions; +using FluentAssertions.Extensions; + +namespace CodingTracker.Tests.Helpers; + +public class HelperTests +{ + private readonly Helper _helper; + + public HelperTests() + { + _helper = new Helper(); + } + + [Fact] + public void ValidateDate_ValidFormat_ReturnsTrue() + { + //Arrange + string validDate = "08/12/2025 14:30:00"; + DateTime parsedDate; + + //Act + bool result = _helper.ValidateDate(validDate, out parsedDate); + + //Assert + result.Should().BeTrue(); + } + + [Fact] + public void ValidateDate_InvalidFormat_ReturnsFalse() + { + //Arrange + string invalidDate = "2025.12.01"; + DateTime parsedDate; + + //Act + bool result = _helper.ValidateDate(invalidDate, out parsedDate); + + //Assert + result.Should().BeFalse(); + } + + [Fact] + public void IsSessionDatesValid_ValidFormat_ReturnsTrue() + { + //Arrange + var startDate = 1.March(2025).At(22, 15).AsLocal(); + var endDate = 2.March(2025).At(22, 15).AsLocal(); + + //Act + bool result = _helper.IsSessionDatesValid(startDate,endDate); + + //Assert + result.Should().BeTrue(); + } + + [Fact] + public void IsSessionDatesValid_InvalidFormat_ReturnsFalse() + { + //Arrange + var startDate = 20.March(2025).At(22, 15).AsLocal(); + var endDate = 1.March(2025).At(22, 15).AsLocal(); + + //Act + bool result = _helper.IsSessionDatesValid(startDate,endDate); + + //Assert + result.Should().BeFalse(); + } +} diff --git a/CodingTracker.Tests/ServiceTests/TrackerServiceTests.cs b/CodingTracker.Tests/ServiceTests/TrackerServiceTests.cs new file mode 100644 index 0000000..a3b516d --- /dev/null +++ b/CodingTracker.Tests/ServiceTests/TrackerServiceTests.cs @@ -0,0 +1,51 @@ +using CodingTracker.kilozdazolik.Models; +using CodingTracker.kilozdazolik.Services; +using Dapper; +using FluentAssertions; +using FluentAssertions.Extensions; +using Microsoft.Data.Sqlite; + +namespace CodingTracker.Tests.Services; + +public class TrackerServiceTests : IDisposable +{ + private const string TestConnectionString = "Data Source=CodingTrackerTest;Mode=Memory;Cache=Shared"; + private readonly SqliteConnection _keepAliveConnection; + + public TrackerServiceTests() + { + _keepAliveConnection = new SqliteConnection(TestConnectionString); + _keepAliveConnection.Open(); + _keepAliveConnection.Execute(@" + CREATE TABLE IF NOT EXISTS CodingTracker ( + Id INTEGER PRIMARY KEY AUTOINCREMENT, + StartTime TEXT, + EndTime TEXT, + Duration TEXT + )"); + } + + [Fact] + public void InsertSession_StoresData_InMemory() + { + //Arrange + var service = new TrackerService(TestConnectionString); + + var startTime = DateTime.Now; + var endTime = startTime.AddHours(5); + + //Act + service.InsertSession(startTime, endTime); + + //Assert + var result = _keepAliveConnection.QuerySingle("SELECT * FROM CodingTracker"); + + result.Should().NotBeNull(); + result.Duration.Should().NotBe(200.Milliseconds()); + } + + public void Dispose() + { + _keepAliveConnection.Close(); + } +} \ No newline at end of file diff --git a/CodingTracker.Tests/obj/Debug/net10.0/CodingTracker.Tests.GlobalUsings.g.cs b/CodingTracker.Tests/obj/Debug/net10.0/CodingTracker.Tests.GlobalUsings.g.cs new file mode 100644 index 0000000..fe43752 --- /dev/null +++ b/CodingTracker.Tests/obj/Debug/net10.0/CodingTracker.Tests.GlobalUsings.g.cs @@ -0,0 +1,9 @@ +// +global using System; +global using System.Collections.Generic; +global using System.IO; +global using System.Linq; +global using System.Net.Http; +global using System.Threading; +global using System.Threading.Tasks; +global using Xunit; diff --git a/CodingTracker.kilozdazolik/CodingTracker.kilozdazolik.csproj b/CodingTracker.kilozdazolik/CodingTracker.kilozdazolik.csproj new file mode 100644 index 0000000..737c485 --- /dev/null +++ b/CodingTracker.kilozdazolik/CodingTracker.kilozdazolik.csproj @@ -0,0 +1,24 @@ + + + + Exe + net10.0 + enable + enable + + + + + + + + + + + + + PreserveNewest + + + + diff --git a/CodingTracker.kilozdazolik/Controllers/TrackerController.cs b/CodingTracker.kilozdazolik/Controllers/TrackerController.cs new file mode 100644 index 0000000..efdb733 --- /dev/null +++ b/CodingTracker.kilozdazolik/Controllers/TrackerController.cs @@ -0,0 +1,173 @@ +using System.Diagnostics; +using CodingTracker.kilozdazolik.Services; +using Spectre.Console; +using CodingTracker.kilozdazolik.Models; +namespace CodingTracker.kilozdazolik.Controllers; + +public class TrackerController +{ + private readonly ITrackerService _trackerService; + private readonly Helper _helper = new(); + + public TrackerController(ITrackerService trackerService) + { + _trackerService = trackerService; + } + + public void StartSession() + { + AnsiConsole.MarkupLine("Press the Enter key to begin/stop:"); + Console.ReadLine(); + DateTime startTime = DateTime.Now; + Stopwatch sw = Stopwatch.StartNew(); + + while (!Console.KeyAvailable || Console.ReadKey(true).Key != ConsoleKey.Enter) + { + Console.Clear(); + TimeSpan ts = sw.Elapsed; + AnsiConsole.Markup($"Elapsed time: {ts.Hours:D2}:{ts.Minutes:D2}:{ts.Seconds:D2}"); + Thread.Sleep(1000); + } + + sw.Stop(); + DateTime endTime = DateTime.Now; + + Console.Clear(); + AnsiConsole.MarkupLine($"Final elapsed time: {sw.Elapsed.Hours:D2}:{sw.Elapsed.Minutes:D2}:{sw.Elapsed.Seconds:D2}"); + + _trackerService.InsertSession(startTime, endTime); + + AnsiConsole.MarkupLine("Press Any Key to Continue."); + Console.ReadKey(); + } + public void AddSession() + { + bool confirm = false; + while (!confirm) + { + AnsiConsole.MarkupLine("[yellow]Add a new coding session[/]"); + + var inputStartDate = AnsiConsole.Prompt( + new TextPrompt("Please write the starting date in this format: (dd/MM/yyyy HH:mm:ss)")); + if (!_helper.ValidateDate(inputStartDate, out DateTime startDate)) + { + AnsiConsole.MarkupLine("[red]Invalid start date format![/]"); + continue; + } + + var inputEndDate = AnsiConsole.Prompt( + new TextPrompt("Please write the ending date in this format: (dd/MM/yyyy HH:mm:ss)")); + if (!_helper.ValidateDate(inputEndDate, out DateTime endDate)) + { + AnsiConsole.MarkupLine("[red]Invalid start date format![/]"); + continue; + } + + if (_helper.IsSessionDatesValid(startDate, endDate)) + { + _trackerService.InsertSession(startDate, endDate); + confirm = true; + AnsiConsole.MarkupLine("[green]Session successfully added![/]"); + } + } + } + + public void ViewAllSessions() + { + List allSessions = _trackerService.GetAllSession(); + + if (allSessions.Any()) + { + _helper.CreateTable(allSessions); + } + else + { + AnsiConsole.MarkupLine("I could not find any session."); + } + + } + public void DeleteSession() + { + List allSession = _trackerService.GetAllSession(); + + if (allSession.Count == 0) + { + AnsiConsole.MarkupLine("[red]No session is available to delete.[/]"); + Console.ReadKey(); + } + + var sessionToDelete = AnsiConsole.Prompt(new SelectionPrompt() + .Title("Select a [red]session[/] to delete:").UseConverter(s => $"Start: {s.StartTime} | End: {s.EndTime} - {s.Duration} elapsed.").AddChoices(allSession)); + + if (_helper.ConfirmMessage("Delete", sessionToDelete.StartTime.ToString())) + { + _trackerService.DeleteSession(sessionToDelete); + } + else + { + AnsiConsole.MarkupLine("Deletion Canceled."); + } + + AnsiConsole.MarkupLine("[green]Press Any Key to Continue.[/]"); + Console.ReadKey(); + } + public void EditSession() + { + List allSession = _trackerService.GetAllSession(); + + if (allSession.Count == 0) + { + AnsiConsole.MarkupLine("[red]No session is available to delete.[/]"); + Console.ReadKey(); + } + + var sessionToEdit = AnsiConsole.Prompt(new SelectionPrompt() + .Title("Select a [cyan]session[/] to edit:").UseConverter(s => $"Start: {s.StartTime} | End: {s.EndTime} - {s.Duration} elapsed.").AddChoices(allSession)); + + Console.Clear(); + + bool confirm = false; + while (!confirm) + { + var inputStartDate = AnsiConsole.Prompt( + new TextPrompt($"Enter the [green]new start time[/] (dd/MM/yyyy HH:mm:ss)\n[grey](default: {sessionToEdit.StartTime:dd/MM/yyyy HH:mm:ss})[/]")); + if (!_helper.ValidateDate(inputStartDate, out DateTime newStartingTime)) + { + AnsiConsole.MarkupLine("[red]Invalid start date format![/]"); + continue; + } + + var inputEndDate = AnsiConsole.Prompt( + new TextPrompt($"Enter the [green]new end time[/] (dd/MM/yyyy HH:mm:ss)\n[grey](default: {sessionToEdit.EndTime:dd/MM/yyyy HH:mm:ss})[/]")); + if (!_helper.ValidateDate(inputEndDate, out DateTime newEndingTime)) + { + AnsiConsole.MarkupLine("[red]Invalid end date format![/]"); + continue; + } + + if (_helper.IsSessionDatesValid(newStartingTime, newEndingTime)) + { + sessionToEdit.StartTime = newStartingTime; + sessionToEdit.EndTime = newEndingTime; + confirm = true; + } + } + + _trackerService.UpdateSession(sessionToEdit); + } + + public void ViewSessionsByDate(bool ascending) + { + List allSessions = _trackerService.GetSessionsOrderByDate(ascending); + + if (allSessions.Any()) + { + _helper.CreateTable(allSessions); + } + else + { + AnsiConsole.MarkupLine("I could not find any session."); + } + + } +} \ No newline at end of file diff --git a/CodingTracker.kilozdazolik/Data/Database.cs b/CodingTracker.kilozdazolik/Data/Database.cs new file mode 100644 index 0000000..dbe6200 --- /dev/null +++ b/CodingTracker.kilozdazolik/Data/Database.cs @@ -0,0 +1,82 @@ +using System.Data; +using Dapper; +using Microsoft.Data.Sqlite; +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.Configuration.Json; + +namespace CodingTracker.kilozdazolik.Data; + +internal static class Database +{ + private static readonly IConfiguration _config; + + static Database() + { + _config = new ConfigurationBuilder().SetBasePath(Directory.GetCurrentDirectory()) + .AddJsonFile("appsettings.json").Build(); + } + + public static string ConnectionString(string name) + { + return _config.GetConnectionString(name); + } + + public static void CreateDatabase() + { + using (IDbConnection db = new SqliteConnection(ConnectionString("DefaultConnection"))) + { + string createTable = @" + CREATE TABLE IF NOT EXISTS CodingTracker( + ID INTEGER PRIMARY KEY, + StartTime TEXT NOT NULL, + EndTime TEXT NOT NULL, + Duration TEXT + )"; + + db.Execute(createTable); + } + } + + public static void InsertDummyData() +{ + using (IDbConnection db = new SqliteConnection(ConnectionString("DefaultConnection"))) + { + int existingCount = db.QuerySingle("SELECT COUNT(*) FROM CodingTracker"); + + if (existingCount > 0) + { + return; + } + + var random = new Random(); + var insertSql = @" + INSERT INTO CodingTracker (StartTime, EndTime, Duration) + VALUES (@StartTime, @EndTime, @Duration)"; + + var dummyData = new List(); + + for (int i = 0; i < 50; i++) + { + var baseDate = DateTime.Now.AddMonths(-6).AddDays(random.Next(0, 180)); + + var startTime = baseDate.Date.AddHours(random.Next(6, 23)).AddMinutes(random.Next(0, 60)); + + var durationMinutes = random.Next(15, 480); // 15 perc - 8 óra + var endTime = startTime.AddMinutes(durationMinutes); + + var duration = TimeSpan.FromMinutes(durationMinutes); + var durationString = $"{(int)duration.TotalHours:D2}:{duration.Minutes:D2}:{duration.Seconds:D2}"; + + dummyData.Add(new + { + StartTime = startTime.ToString("yyyy-MM-dd HH:mm:ss"), + EndTime = endTime.ToString("yyyy-MM-dd HH:mm:ss"), + Duration = durationString + }); + } + + db.Execute(insertSql, dummyData); + } +} + +} \ No newline at end of file diff --git a/CodingTracker.kilozdazolik/Enums/Enum.cs b/CodingTracker.kilozdazolik/Enums/Enum.cs new file mode 100644 index 0000000..50cf9fe --- /dev/null +++ b/CodingTracker.kilozdazolik/Enums/Enum.cs @@ -0,0 +1,18 @@ +namespace CodingTracker.kilozdazolik.Enums; + +internal enum MenuAction +{ + StartSession, + AddSession, + ViewSessions, + DeleteSessions, + EditSessions, + Quit +} + +internal enum ViewAction +{ + AllSession, + AscendingOrder, + DescendingOrder, +} \ No newline at end of file diff --git a/CodingTracker.kilozdazolik/Helper.cs b/CodingTracker.kilozdazolik/Helper.cs new file mode 100644 index 0000000..2a723f6 --- /dev/null +++ b/CodingTracker.kilozdazolik/Helper.cs @@ -0,0 +1,65 @@ +using System.Globalization; +using CodingTracker.kilozdazolik.Models; +using Spectre.Console; +namespace CodingTracker.kilozdazolik; + +public class Helper +{ + public bool ConfirmMessage(string message, string element, string color = "red") + { + var confirm = AnsiConsole.Confirm($"Are you sure you want to {message} [{color}]{element}[/]?"); + + return confirm; + } + + public bool ValidateDate(string date, out DateTime parsedDate) + { + return DateTime.TryParseExact(date, "dd/MM/yyyy HH:mm:ss", + CultureInfo.InvariantCulture, DateTimeStyles.None, out parsedDate); + } + + public bool IsSessionDatesValid(DateTime startDate, DateTime endDate) + { + int result = DateTime.Compare(startDate, endDate); + + if (result > 0) + { + AnsiConsole.MarkupLine("The end date must not precede the start date!"); + return false; + } + + return true; + } + + public void CreateTable(List allSessions) + { + var table = new Table(); + table.Border(TableBorder.Rounded); + + table.AddColumn("[yellow]ID[/]"); + table.AddColumn("[yellow]Starting date[/]"); + table.AddColumn("[yellow]Ending date[/]"); + table.AddColumn("[yellow]Duration[/]"); + + foreach (var session in allSessions) + { + table.AddRow( + session.Id.ToString(), + $"[blue]{session.StartTime.ToString("dd-MM-yyyy HH:mm", CultureInfo.GetCultureInfo("en-US"))}[/]", + $"[cyan]{session.EndTime.ToString("dd-MM-yyyy HH:mm", CultureInfo.GetCultureInfo("en-US"))}[/]", + $"[green]{session.Duration}[/]" + ); + } + + AnsiConsole.Write(table); + } + + public void ShowWithPause(Action action) + { + AnsiConsole.Clear(); + action(); + AnsiConsole.MarkupLine("\n[grey]Press any key to return...[/]"); + Console.ReadKey(true); + AnsiConsole.Clear(); + } +} \ No newline at end of file diff --git a/CodingTracker.kilozdazolik/Models/TrackerModel.cs b/CodingTracker.kilozdazolik/Models/TrackerModel.cs new file mode 100644 index 0000000..9c60d06 --- /dev/null +++ b/CodingTracker.kilozdazolik/Models/TrackerModel.cs @@ -0,0 +1,9 @@ +namespace CodingTracker.kilozdazolik.Models; + +public class Tracker +{ + public int Id { get; set; } + public DateTime StartTime { get; set; } + public DateTime EndTime { get; set; } + public TimeSpan Duration => EndTime - StartTime; +} \ No newline at end of file diff --git a/CodingTracker.kilozdazolik/Program.cs b/CodingTracker.kilozdazolik/Program.cs new file mode 100644 index 0000000..d5e64a8 --- /dev/null +++ b/CodingTracker.kilozdazolik/Program.cs @@ -0,0 +1,6 @@ +using CodingTracker.kilozdazolik; +using CodingTracker.kilozdazolik.Data; + +Database.CreateDatabase(); +Database.InsertDummyData(); +UserInterface.MainMenu(); \ No newline at end of file diff --git a/CodingTracker.kilozdazolik/Services/ITrackerService.cs b/CodingTracker.kilozdazolik/Services/ITrackerService.cs new file mode 100644 index 0000000..bd782b7 --- /dev/null +++ b/CodingTracker.kilozdazolik/Services/ITrackerService.cs @@ -0,0 +1,12 @@ +using CodingTracker.kilozdazolik.Models; + +namespace CodingTracker.kilozdazolik.Services; + +public interface ITrackerService +{ + void InsertSession(DateTime startDate, DateTime endDate); + List GetAllSession(); + void DeleteSession(Tracker tracker); + void UpdateSession(Tracker tracker); + List GetSessionsOrderByDate(bool ascending); +} \ No newline at end of file diff --git a/CodingTracker.kilozdazolik/Services/TrackerService.cs b/CodingTracker.kilozdazolik/Services/TrackerService.cs new file mode 100644 index 0000000..8171d8c --- /dev/null +++ b/CodingTracker.kilozdazolik/Services/TrackerService.cs @@ -0,0 +1,116 @@ +using System.Data; +using Microsoft.Data.Sqlite; +using CodingTracker.kilozdazolik.Data; +using CodingTracker.kilozdazolik.Models; +using Dapper; + +namespace CodingTracker.kilozdazolik.Services; + +public class TrackerService : ITrackerService +{ + private readonly string _connectionString; + + public TrackerService(string connectionString) + { + _connectionString = connectionString; + } + + private IDbConnection CreateConnection() + { + return new SqliteConnection(_connectionString); + } + + public void InsertSession(DateTime startDate, DateTime endDate) + { + try + { + using (var conn = CreateConnection()) + { + var sql = + "INSERT INTO CodingTracker (StartTime, EndTime, Duration) VALUES (@StartTime, @EndTime, @Duration)"; + + Tracker tracker = new() + { + StartTime = startDate, + EndTime = endDate, + }; + + conn.Execute(sql, tracker); + } + } + catch (SqliteException ex) + { + throw new Exception("Database operation failed", ex); + } + } + + public List GetAllSession() + { + try + { + using (var conn = CreateConnection()) + { + var sql = + "SELECT * FROM CodingTracker"; + var sessions = conn.Query(sql).ToList(); + return sessions; + } + } + catch (SqliteException ex) + { + Console.WriteLine($"Unexpected error happened: {ex.Message}"); + return new List(); + } + } + + public void DeleteSession(Tracker tracker) + { + try + { + using (var conn = CreateConnection()) + { + conn.Execute("DELETE FROM CodingTracker WHERE ID = @Id", tracker); + } + } + catch (SqliteException ex) + { + throw new Exception("Database operation failed", ex); + } + } + + public void UpdateSession(Tracker tracker) + { + try + { + using (var conn = CreateConnection()) + { + conn.Execute("UPDATE CodingTracker SET StartTime = @StartTime, EndTime = @EndTime WHERE ID = @Id", tracker); + } + } + catch (SqliteException ex) + { + throw new Exception("Database operation failed", ex); + } + } + + // FILTER METHODS + public List GetSessionsOrderByDate(bool ascending) + { + try + { + using (var conn = CreateConnection()) + { + var orderDirection = ascending ? "ASC" : "DESC"; + var sql = + $"SELECT * FROM CodingTracker ORDER BY StartTime {orderDirection}"; + var sessions = conn.Query(sql).ToList(); + return sessions; + } + } + catch (SqliteException ex) + { + Console.WriteLine($"Unexpected error happened: {ex.Message}"); + return new List(); + } + } +} \ No newline at end of file diff --git a/CodingTracker.kilozdazolik/UserInterface.cs b/CodingTracker.kilozdazolik/UserInterface.cs new file mode 100644 index 0000000..c1e2bb7 --- /dev/null +++ b/CodingTracker.kilozdazolik/UserInterface.cs @@ -0,0 +1,72 @@ +using CodingTracker.kilozdazolik.Controllers; +using CodingTracker.kilozdazolik.Data; +using Spectre.Console; +using CodingTracker.kilozdazolik.Enums; +using CodingTracker.kilozdazolik.Services; + +namespace CodingTracker.kilozdazolik; + +public class UserInterface +{ + private static Helper _helper = new(); + + private static readonly ITrackerService _service = new TrackerService(Database.ConnectionString("DefaultConnection")); + private static readonly TrackerController _tracker = new TrackerController(_service); + + internal static void MainMenu() + { + while (true) { + var choice = AnsiConsole.Prompt( + new SelectionPrompt() + .Title("What do you want to do [green]next[/]?") + .PageSize(10) + .MoreChoicesText("[grey](Move up and down to choose an option)[/]") + .AddChoices(Enum.GetValues())); + + switch (choice) + { + case MenuAction.StartSession: + _helper.ShowWithPause(_tracker.StartSession); + break; + case MenuAction.AddSession: + _helper.ShowWithPause(_tracker.AddSession); + break; + case MenuAction.ViewSessions: + ViewOptions(); + break; + case MenuAction.DeleteSessions: + _helper.ShowWithPause(_tracker.DeleteSession); + break; + case MenuAction.EditSessions: + _helper.ShowWithPause(_tracker.EditSession); + break; + + case MenuAction.Quit: + return; + } + } + } + + private static void ViewOptions() + { + var filterChoice = AnsiConsole.Prompt( + new SelectionPrompt() + .Title("What do you want to do [green]next[/]?") + .PageSize(10) + .MoreChoicesText("[grey](Move up and down to choose an option)[/]") + .AddChoices(Enum.GetValues())); + + switch (filterChoice) + { + case ViewAction.AllSession: + _helper.ShowWithPause(_tracker.ViewAllSessions); + break; + case ViewAction.AscendingOrder: + _helper.ShowWithPause(() => _tracker.ViewSessionsByDate(true)); + break; + case ViewAction.DescendingOrder: + _helper.ShowWithPause(() => _tracker.ViewSessionsByDate(false)); + break; + } + } +} \ No newline at end of file diff --git a/CodingTracker.kilozdazolik/appsettings.json b/CodingTracker.kilozdazolik/appsettings.json new file mode 100644 index 0000000..182f6fd --- /dev/null +++ b/CodingTracker.kilozdazolik/appsettings.json @@ -0,0 +1,5 @@ +{ + "ConnectionStrings": { + "DefaultConnection": "Data Source=CodingTracker.db" + } +} \ No newline at end of file diff --git a/UnitTests.kilozdazolik.sln b/UnitTests.kilozdazolik.sln new file mode 100644 index 0000000..5d2d150 --- /dev/null +++ b/UnitTests.kilozdazolik.sln @@ -0,0 +1,22 @@ + +Microsoft Visual Studio Solution File, Format Version 12.00 +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "CodingTracker.kilozdazolik", "CodingTracker.kilozdazolik\CodingTracker.kilozdazolik.csproj", "{D3E51C0D-1DE8-4E1F-A77B-B063EF29E7FF}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "CodingTracker.Tests", "CodingTracker.Tests\CodingTracker.Tests.csproj", "{FD1F42B6-89FB-43AE-84F5-57FCB0E0D7AF}" +EndProject +Global + GlobalSection(SolutionConfigurationPlatforms) = preSolution + Debug|Any CPU = Debug|Any CPU + Release|Any CPU = Release|Any CPU + EndGlobalSection + GlobalSection(ProjectConfigurationPlatforms) = postSolution + {D3E51C0D-1DE8-4E1F-A77B-B063EF29E7FF}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {D3E51C0D-1DE8-4E1F-A77B-B063EF29E7FF}.Debug|Any CPU.Build.0 = Debug|Any CPU + {D3E51C0D-1DE8-4E1F-A77B-B063EF29E7FF}.Release|Any CPU.ActiveCfg = Release|Any CPU + {D3E51C0D-1DE8-4E1F-A77B-B063EF29E7FF}.Release|Any CPU.Build.0 = Release|Any CPU + {FD1F42B6-89FB-43AE-84F5-57FCB0E0D7AF}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {FD1F42B6-89FB-43AE-84F5-57FCB0E0D7AF}.Debug|Any CPU.Build.0 = Debug|Any CPU + {FD1F42B6-89FB-43AE-84F5-57FCB0E0D7AF}.Release|Any CPU.ActiveCfg = Release|Any CPU + {FD1F42B6-89FB-43AE-84F5-57FCB0E0D7AF}.Release|Any CPU.Build.0 = Release|Any CPU + EndGlobalSection +EndGlobal From 2b310338c97e5ec516ad6a97a4f5449355a6f338 Mon Sep 17 00:00:00 2001 From: kilozdazolik Date: Mon, 8 Dec 2025 20:51:37 +0100 Subject: [PATCH 2/3] remove .idea --- .../.idea.UnitTests.kilozdazolik/.idea/.gitignore | 15 --------------- 1 file changed, 15 deletions(-) delete mode 100644 .idea/.idea.UnitTests.kilozdazolik/.idea/.gitignore diff --git a/.idea/.idea.UnitTests.kilozdazolik/.idea/.gitignore b/.idea/.idea.UnitTests.kilozdazolik/.idea/.gitignore deleted file mode 100644 index c8b60a7..0000000 --- a/.idea/.idea.UnitTests.kilozdazolik/.idea/.gitignore +++ /dev/null @@ -1,15 +0,0 @@ -# Default ignored files -/shelf/ -/workspace.xml -# Rider ignored files -/modules.xml -/.idea.UnitTests.kilozdazolik.iml -/projectSettingsUpdater.xml -/contentModel.xml -# Ignored default folder with query files -/queries/ -# Datasource local storage ignored files -/dataSources/ -/dataSources.local.xml -# Editor-based HTTP Client requests -/httpRequests/ From dc2efbed57017c939fb2bce673b5ca086b59d7d1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?P=C3=A1lfy=20Zolt=C3=A1n?= Date: Mon, 8 Dec 2025 20:54:13 +0100 Subject: [PATCH 3/3] Add README for Coding Tracker application This README provides an overview of the Coding Tracker application, detailing its features, technologies used, testing strategies, and setup instructions. --- README.md | 47 +++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 47 insertions(+) create mode 100644 README.md diff --git a/README.md b/README.md new file mode 100644 index 0000000..018495d --- /dev/null +++ b/README.md @@ -0,0 +1,47 @@ +# Coding Tracker 🕒 + +A console-based application allowing users to track their coding sessions. This project helps monitor coding habits by recording start and end times, calculating durations, and saving data to a local database. + +## 🚀 Features + +* **Start/Stop Session:** Use a stopwatch to record a live coding session. +* **Manual Entry:** Manually input start and end times with validation. +* **View History:** View all past sessions in a formatted table. +* **Update/Delete:** Modify or remove specific sessions. +* **Filtering:** Order sessions by date (ascending/descending). +* **Data Persistence:** All data is saved in a SQLite database. + +## 🛠️ Technologies & Patterns + +* **Language:** C# (.NET) +* **Database:** SQLite +* **ORM:** Dapper (for efficient data access) +* **UI:** Spectre.Console (for a beautiful console interface) +* **Testing:** + * **xUnit:** Test runner. + * **FakeItEasy:** Used for mocking dependencies. + * **FluentAssertions:** For readable test assertions. +* **Architecture:** + * Implements **Dependency Injection** to decouple the Controller from the Service. + * Separation of concerns (UI, Controller, Service, Data layers). + +## 🧪 Testing + +The project includes a comprehensive test suite: +1. **Unit Tests:** Validates helper logic (date parsing) and Controller logic (using Mocks to isolate the UI from the Database). +2. **Integration Tests:** Verifies database operations to ensure data is correctly inserted and retrieved without polluting the local file system. + +## 🏁 Getting Started + +1. Clone the repository. +2. Open the solution in Visual Studio or Rider. +3. Ensure the `appsettings.json` (or `app.config`) contains the correct connection string for SQLite. +4. Run the application. The database table will be created automatically on the first run. + +## 🧠 Challenges & Lessons Learned + +* **Testing Legacy Code:** Refactored the application to use **Dependency Injection**, transforming a hard-to-test tightly coupled architecture into a testable, modular design. +* **Mocking:** Learned how to use Mock objects to test business logic without relying on the actual database or user input. + +--- +*Created as part of the C# Academy curriculum.*