diff --git a/CodingTrackerApp.JJHH17/CodingTrackerApp.JJHH17.Tests/AppTests.cs b/CodingTrackerApp.JJHH17/CodingTrackerApp.JJHH17.Tests/AppTests.cs new file mode 100644 index 0000000..3a5a7b8 --- /dev/null +++ b/CodingTrackerApp.JJHH17/CodingTrackerApp.JJHH17.Tests/AppTests.cs @@ -0,0 +1,47 @@ +using Microsoft.VisualStudio.TestTools.UnitTesting; +using CodingTrackerApp.JJHH17.Models; +using System; + +namespace CodingTrackerApp.JJHH17.Tests; + +[TestClass] +public class AppTests +{ + [TestMethod] + public void Duration_CalculateDurationCorrectly() + { + var start = "2024-01-01 10:00:00"; + var end = "2024-01-01 12:30:45"; + var session = new CodingTrackerApp.JJHH17.Models.CodingSession(start, end); + + Assert.AreEqual("0 years, 0 months, 0 days, 2 hours, 30 minutes, 45 seconds", session.GetDuration()); + } + + [TestMethod] + public void Duration_InvalidDurationCalculation_PassesIfTestPasses() + { + var start = "2024-01-01 10:00:00"; + var end = "2024-01-01 11:00:00"; + var session = new CodingTrackerApp.JJHH17.Models.CodingSession(start, end); + + Assert.AreNotEqual("0 years, 0 months, 0 days, 3 hours, 0 minutes, 0 seconds", session.GetDuration()); + } + + [TestMethod] + public void Duration_ZeroDuration_PassesIfTestPasses() + { + var start = "2024-01-01 10:00:00"; + var end = "2024-01-01 10:00:00"; + var session = new CodingTrackerApp.JJHH17.Models.CodingSession(start, end); + Assert.AreEqual("0 years, 0 months, 0 days, 0 hours, 0 minutes, 0 seconds", session.GetDuration()); + } + + [TestMethod] + public void DateInput_ValidDateFormat_PassesIfValidFormat() + { + var dateString = "2024-06-15 14:30"; + DateTime parsedDate; + var isValid = DateTime.TryParse(dateString, out parsedDate); + Assert.IsTrue(isValid); + } +} \ No newline at end of file diff --git a/CodingTrackerApp.JJHH17/CodingTrackerApp.JJHH17.Tests/CodingTrackerApp.JJHH17.Tests.csproj b/CodingTrackerApp.JJHH17/CodingTrackerApp.JJHH17.Tests/CodingTrackerApp.JJHH17.Tests.csproj new file mode 100644 index 0000000..1bf2f0f --- /dev/null +++ b/CodingTrackerApp.JJHH17/CodingTrackerApp.JJHH17.Tests/CodingTrackerApp.JJHH17.Tests.csproj @@ -0,0 +1,20 @@ + + + net8.0 + false + + + + + + + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + + + + + + + diff --git a/CodingTrackerApp.JJHH17/CodingTrackerApp.JJHH17/CodingTrackerApp.JJHH17.csproj b/CodingTrackerApp.JJHH17/CodingTrackerApp.JJHH17/CodingTrackerApp.JJHH17.csproj new file mode 100644 index 0000000..4363f1d --- /dev/null +++ b/CodingTrackerApp.JJHH17/CodingTrackerApp.JJHH17/CodingTrackerApp.JJHH17.csproj @@ -0,0 +1,19 @@ + + + + Exe + net8.0 + enable + enable + + + + + + + + + + + + diff --git a/CodingTrackerApp.JJHH17/CodingTrackerApp.JJHH17/Database/App.config b/CodingTrackerApp.JJHH17/CodingTrackerApp.JJHH17/Database/App.config new file mode 100644 index 0000000..3ad7955 --- /dev/null +++ b/CodingTrackerApp.JJHH17/CodingTrackerApp.JJHH17/Database/App.config @@ -0,0 +1,7 @@ + + + + + + + \ No newline at end of file diff --git a/CodingTrackerApp.JJHH17/CodingTrackerApp.JJHH17/Database/Database.cs b/CodingTrackerApp.JJHH17/CodingTrackerApp.JJHH17/Database/Database.cs new file mode 100644 index 0000000..9e00ebc --- /dev/null +++ b/CodingTrackerApp.JJHH17/CodingTrackerApp.JJHH17/Database/Database.cs @@ -0,0 +1,95 @@ +using Dapper; +using System.Configuration; +using System.Data.SQLite; +using CodingTrackerApp.JJHH17.Models; + +namespace CodingTrackerApp.JJHH17.Database; + +public class Database +{ + private static readonly string dbPath = ConfigurationManager.AppSettings["databasePath"]; + private static readonly string tableName = ConfigurationManager.AppSettings["tableName"]; + private static readonly string connectionString = $"Data Source={dbPath};"; + + public static void CreateDatabase() + { + if (!File.Exists(dbPath)) + { + SQLiteConnection.CreateFile(dbPath); + Console.WriteLine("Database created successfully"); + } + else + { + Console.WriteLine("Database already exists"); + } + + CreateTable(); + } + + private static void CreateTable() + { + using (var connection = new SQLiteConnection(connectionString)) + { + connection.Open(); + + string tableCreation = @"CREATE TABLE IF NOT EXISTS CodeTracker ( + Id INTEGER PRIMARY KEY AUTOINCREMENT, + StartTime TEXT NOT NULL, + EndTime TEXT NOT NULL, + Duration TEXT);"; + + connection.Execute(tableCreation); + Console.WriteLine("Table created successfully or already exists"); + } + } + + public static long AddEntry(string startTime, string endTime, string duration) + { + using (var connection = new SQLiteConnection(connectionString)) + { + connection.Open(); + var sql = "INSERT INTO CodeTracker (StartTime, EndTime, Duration) VALUES (@StartTime, @EndTime, @Duration);" + + $"SELECT last_insert_rowid();"; + + var newEntry = new CodingSession(startTime, endTime, duration); + + long newId = connection.ExecuteScalar(sql, newEntry); + + return newId; + } + } + + public static List GetAllEntries() + { + using (var connection = new SQLiteConnection(connectionString)) + { + connection.Open(); + var sql = "SELECT * FROM CodeTracker;"; + var entries = connection.Query(sql).ToList(); + return entries; + } + } + + public static void DeleteAllEntries() + { + using (var connection = new SQLiteConnection(connectionString)) + { + connection.Open(); + var sql = $"DELETE FROM {tableName};"; + connection.Execute(sql); + } + } + + public static void DeleteEntryById(long id) + { + using (var connection = new SQLiteConnection(connectionString)) + { + // Get list of existing entries for ID selection + GetAllEntries(); + + connection.Open(); + var sql = $"DELETE FROM {tableName} WHERE Id = @Id;"; + connection.Execute(sql, new { Id = id } ); + } + } +} \ No newline at end of file diff --git a/CodingTrackerApp.JJHH17/CodingTrackerApp.JJHH17/Models/CodingSession.cs b/CodingTrackerApp.JJHH17/CodingTrackerApp.JJHH17/Models/CodingSession.cs new file mode 100644 index 0000000..21c2034 --- /dev/null +++ b/CodingTrackerApp.JJHH17/CodingTrackerApp.JJHH17/Models/CodingSession.cs @@ -0,0 +1,67 @@ +namespace CodingTrackerApp.JJHH17.Models; + +public class CodingSession +{ + public long Id { get; set; } + public string StartTime { get; set; } + public string EndTime { get; set; } + public string Duration { get; set; } + + public DateTime? stopwatchStartTime; + public DateTime? stopwatchEndTime; + + public CodingSession() + { + } + + public CodingSession(string startTime, string endTime) + { + StartTime = startTime; + EndTime = endTime; + CalculateDuration(); + } + + public CodingSession(string startTime, string endTime, string duration) + { + StartTime = startTime; + EndTime = endTime; + Duration = duration; + } + + public void CalculateDuration() + { + DateTime start = DateTime.Parse(StartTime); + DateTime end = DateTime.Parse(EndTime); + + int years = end.Year - start.Year; + int months = end.Month - start.Month; + int days = end.Day - start.Day; + + if (days < 0) + { + months--; + var previousMonth = end.AddMonths(-1); + days += DateTime.DaysInMonth(previousMonth.Year, previousMonth.Month); + } + + if (months < 0) + { + years--; + months += 12; + } + + TimeSpan timespan = end - start; + + int totalDays = (int)timespan.TotalDays; + int totalHours = (int)timespan.TotalHours % 24; + int totalMinutes = (int)timespan.TotalMinutes % 60; + int totalSeconds = (int)timespan.TotalSeconds % 60; + + Duration = $"{years} years, {months} months, {days} days, {totalHours} hours, {totalMinutes} minutes, {totalSeconds} seconds"; + } + + public string GetDuration() + { + return Duration; + } +} \ No newline at end of file diff --git a/CodingTrackerApp.JJHH17/CodingTrackerApp.JJHH17/Program.cs b/CodingTrackerApp.JJHH17/CodingTrackerApp.JJHH17/Program.cs new file mode 100644 index 0000000..0f2aa1d --- /dev/null +++ b/CodingTrackerApp.JJHH17/CodingTrackerApp.JJHH17/Program.cs @@ -0,0 +1,12 @@ +using Menu = CodingTrackerApp.JJHH17.UserInterface; + +namespace CodingTrackerApp.JJHH17; + +class Program +{ + public static void Main(string[] args) + { + Database.Database.CreateDatabase(); + Menu.Running(); + } +} \ No newline at end of file diff --git a/CodingTrackerApp.JJHH17/CodingTrackerApp.JJHH17/UserInterface/UserInterface.cs b/CodingTrackerApp.JJHH17/CodingTrackerApp.JJHH17/UserInterface/UserInterface.cs new file mode 100644 index 0000000..33abf91 --- /dev/null +++ b/CodingTrackerApp.JJHH17/CodingTrackerApp.JJHH17/UserInterface/UserInterface.cs @@ -0,0 +1,141 @@ +using Spectre.Console; +using CodingTrackerApp.JJHH17; +using CodingTrackerApp.JJHH17.Models; + +namespace CodingTrackerApp.JJHH17; + +public class UserInterface +{ + enum MenuOptions + { + AddEvent, + ViewAllEvents, + DeleteAll, + DeleteSingleEvent, + Exit + } + + public static void Running() + { + bool active = true; + + while (active) + { + Console.Clear(); + + var choice = AnsiConsole.Prompt( + new SelectionPrompt() + .Title("Select an option:") + .AddChoices(Enum.GetValues())); + + switch (choice) + { + case MenuOptions.AddEvent: + AnsiConsole.MarkupLine("[green]Add Event selected.[/]"); + AddEntry(); + break; + + case MenuOptions.ViewAllEvents: + AnsiConsole.MarkupLine("[green]View All Events selected.[/]"); + ViewAllEntries(); + break; + + case MenuOptions.DeleteAll: + AnsiConsole.MarkupLine("[green]Delete All selected.[/]"); + DeleteAllEntries(); + break; + + case MenuOptions.DeleteSingleEvent: + AnsiConsole.MarkupLine("[green]Delete Single Event selected.[/]"); + DeleteSingleEntry(); + break; + + case MenuOptions.Exit: + active = false; + break; + } + } + } + + public static void AddEntry() + { + string startTime = AnsiConsole.Ask("Enter start time (YYYY-MM-DD HH:MM) (Time is optional):"); + + DateTime parsedStartTime; + while (!DateTime.TryParse(startTime, out parsedStartTime)) + { + AnsiConsole.MarkupLine("[red]Invalid date format. Please try again.[/]"); + startTime = AnsiConsole.Ask("Enter start time (YYYY-MM-DD HH:MM):"); + } + + string endTime = AnsiConsole.Ask("Enter end time (YYYY-MM-DD HH:MM) (Time is optional)"); + + DateTime parsedEndTime; + while (!DateTime.TryParse(endTime, out parsedEndTime) || parsedEndTime <= parsedStartTime) + { + AnsiConsole.MarkupLine("[red]Invalid date format or end time is before start time. Please try again.[/]"); + endTime = AnsiConsole.Ask("Enter end time (YYYY-MM-DD HH:MM):"); + } + + AnsiConsole.MarkupLine("[green]Entry added successfully! Enter a key to continue[/]"); + Console.ReadKey(); + var newEntry = new CodingSession(startTime, endTime); + newEntry.CalculateDuration(); + Database.Database.AddEntry(newEntry.StartTime, newEntry.EndTime, newEntry.Duration); + } + + public static void ViewAllEntries() + { + var table = new Table(); + table.AddColumn("ID"); + table.AddColumn("Start Time"); + table.AddColumn("End Time"); + table.AddColumn("Duration"); + + List entries = Database.Database.GetAllEntries(); + foreach (var entry in entries) + { + table.AddRow(entry.Id.ToString(), entry.StartTime, entry.EndTime, entry.Duration); + } + + AnsiConsole.Write(table); + AnsiConsole.MarkupLine("[green]Press any key to continue...[/]"); + Console.ReadKey(); + } + + public static void DeleteAllEntries() + { + string confirmation = AnsiConsole.Ask("Are you sure you want to delete all entries? (yes/no):"); + if (confirmation.ToLower() == "yes") + { + Database.Database.DeleteAllEntries(); + AnsiConsole.MarkupLine("[green]All entries deleted successfully! Press any key to continue...[/]"); + } + else + { + AnsiConsole.MarkupLine("[yellow]Deletion cancelled. Press any key to continue...[/]"); + } + + Console.ReadKey(); + } + + public static void DeleteSingleEntry() + { + ViewAllEntries(); + int idToDelete = AnsiConsole.Ask("Enter the ID of the entry to delete:"); + AnsiConsole.MarkupLine($"[red]Are you sure you want to delete entry ID {idToDelete}[/]"); + string confirmation = AnsiConsole.Ask("Type 'yes' to confirm deletion:"); + + if (confirmation.ToLower() == "yes") + { + Database.Database.DeleteEntryById(idToDelete); + AnsiConsole.MarkupLine("[green]Entry deleted successfully! Press any key to continue...[/]"); + } + else + { + AnsiConsole.MarkupLine("[yellow]Deletion cancelled. Press any key to continue...[/]"); + } + + Console.ReadKey(); + } +} \ No newline at end of file diff --git a/CodingTrackerApp.JJHH17/README.md b/CodingTrackerApp.JJHH17/README.md new file mode 100644 index 0000000..2ec71d4 --- /dev/null +++ b/CodingTrackerApp.JJHH17/README.md @@ -0,0 +1,44 @@ +### Coding Tracking Application - Unit Tests + +A command line based code tracking application - allows the user to enter time studied, which is then stored to a local instance of SQLite. +This project follows the "Unit Testing" project of the CSharpAcademy, found on: https://www.thecsharpacademy.com/project/21/unit-testing. + +## Technologies used and installed + +- SQLite database. +- Dapper (for integrating with SQLite). +- Spectre console (for terminal styling). +- Microsoft.VisualStudio.TestTools.UnitTesting - For unit testing. + +## Installation and Running Steps: +- This project uses SQLite, meaning a local instance needs to exist in order to store data. +- The project (program.cs) file contains a method that creates the database if it doesn't already exist, meaning you don't need to create this manually. +- Furthermore, it will also create the relevant SQL table needed for storing events. + +## Application Details + +- The application can be started by cloning the directory and running the program.cs file. +- You're then presented with a list of options - use the arrow keys to navigate via the terminal options: + +image + +- The user can add entries via the "Add Event" option, which takes in the start and end date and time: + +image + +- View a full list of coding events added. + +image + +- Delete a single event (by the unique event ID) + +image + +- Delete all events on the database. + +image + +## Unit Tests +- This project contains a suite of unit tests, which can be found under the "CodingTrackerApp.JJHH17.Tests" directory. +- To run the tests, cd to the test directory via terminal and run ```dotnet test```. +- This command will execute all tests in the testing class.