diff --git a/.codacy.yml b/.codacy.yml old mode 100644 new mode 100755 diff --git a/.gitignore b/.gitignore old mode 100644 new mode 100755 diff --git a/NoNameLudovik.CodingTracker/.gitignore b/NoNameLudovik.CodingTracker/.gitignore new file mode 100644 index 00000000..3997bead --- /dev/null +++ b/NoNameLudovik.CodingTracker/.gitignore @@ -0,0 +1 @@ +*.db \ No newline at end of file diff --git a/NoNameLudovik.CodingTracker/NoNameLudovik.CodingTracker.sln b/NoNameLudovik.CodingTracker/NoNameLudovik.CodingTracker.sln new file mode 100755 index 00000000..ce717ee0 --- /dev/null +++ b/NoNameLudovik.CodingTracker/NoNameLudovik.CodingTracker.sln @@ -0,0 +1,16 @@ + +Microsoft Visual Studio Solution File, Format Version 12.00 +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "NoNameLudovik.CodingTracker", "NoNameLudovik.CodingTracker\NoNameLudovik.CodingTracker.csproj", "{761778D9-5806-4FEB-A75E-C95264B3D608}" +EndProject +Global + GlobalSection(SolutionConfigurationPlatforms) = preSolution + Debug|Any CPU = Debug|Any CPU + Release|Any CPU = Release|Any CPU + EndGlobalSection + GlobalSection(ProjectConfigurationPlatforms) = postSolution + {761778D9-5806-4FEB-A75E-C95264B3D608}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {761778D9-5806-4FEB-A75E-C95264B3D608}.Debug|Any CPU.Build.0 = Debug|Any CPU + {761778D9-5806-4FEB-A75E-C95264B3D608}.Release|Any CPU.ActiveCfg = Release|Any CPU + {761778D9-5806-4FEB-A75E-C95264B3D608}.Release|Any CPU.Build.0 = Release|Any CPU + EndGlobalSection +EndGlobal diff --git a/NoNameLudovik.CodingTracker/NoNameLudovik.CodingTracker/CodingSession.cs b/NoNameLudovik.CodingTracker/NoNameLudovik.CodingTracker/CodingSession.cs new file mode 100755 index 00000000..cfd7c413 --- /dev/null +++ b/NoNameLudovik.CodingTracker/NoNameLudovik.CodingTracker/CodingSession.cs @@ -0,0 +1,21 @@ +using System.Globalization; + +namespace NoNameLudovik.CodingTracker; + +public class CodingSession +{ + internal int Id{get;set;} + internal string StartTime{get;set;} + internal string EndTime{get;set;} + internal TimeSpan Duration{get; set;} + + internal void CalculateDuration() + { + var endTime = DateTime.ParseExact(EndTime, "dd-MM-yyyy HH:mm", new CultureInfo("en-US"), DateTimeStyles.None); + var startTime = DateTime.ParseExact(StartTime, "dd-MM-yyyy HH:mm", new CultureInfo("en-US"), DateTimeStyles.None); + + Duration = endTime - startTime; + } + + +} \ No newline at end of file diff --git a/NoNameLudovik.CodingTracker/NoNameLudovik.CodingTracker/DataBaseController.cs b/NoNameLudovik.CodingTracker/NoNameLudovik.CodingTracker/DataBaseController.cs new file mode 100755 index 00000000..3a639bd5 --- /dev/null +++ b/NoNameLudovik.CodingTracker/NoNameLudovik.CodingTracker/DataBaseController.cs @@ -0,0 +1,100 @@ +using Microsoft.Extensions.Configuration; +using Dapper; +using Microsoft.Data.Sqlite; + +namespace NoNameLudovik.CodingTracker; + +internal class DataBaseController +{ + static IConfigurationRoot _config = new ConfigurationBuilder().AddJsonFile("appsettings.json", optional:false, reloadOnChange:true).Build(); + static string _projectRoot = Path.GetFullPath( + Path.Combine( + AppContext.BaseDirectory, + "..", "..", "..")); + static string _dbPath = Path.Combine(_projectRoot, _config["dbPath"]); + static SqliteConnection _connection = new SqliteConnection($"Data source = {_dbPath}"); + + internal static void CreatTable() + { + string createTableSqlQuery = + @"CREATE TABLE IF NOT EXISTS codingSessions ( + Id INTEGER PRIMARY KEY AUTOINCREMENT, + StartTime TEXT, + EndTime TEXT);"; + _connection.Execute(createTableSqlQuery); + } + + internal static void Insert(DateTime startTime, DateTime endTime) + { + string insertSqlQuery = + @"INSERT INTO codingSessions (StartTime, EndTime) VALUES (@StartTime, @EndTime)"; + + var newSession = new {StartTime = startTime.ToString("dd-MM-yyyy HH:mm"), EndTime = endTime.ToString("dd-MM-yyyy HH:mm")}; + + try + { + _connection.Execute(insertSqlQuery, newSession); + } + catch (SqliteException error) + { + throw new Exception($"Error while adding new session to database", error); + } + } + + internal static List SelectFromTable() + { + string selectSqlQuery = @"SELECT * FROM codingSessions"; + + var codingSessions = _connection.Query(selectSqlQuery).ToList(); + return codingSessions; + } + + internal static CodingSession SelectFromTable(int sessionId) + { + string selectSqlQuery = @$"SELECT * FROM codingSessions WHERE Id = {sessionId}"; + + var codingSessions = _connection.Query(selectSqlQuery).ToList(); + return codingSessions[0]; + } + + internal static void DeleteFromTable(int sessionId) + { + string deleteSqlQuery = @"DELETE FROM codingSessions WHERE Id = @sessionId"; + try + { + _connection.Execute(deleteSqlQuery, new { sessionId }); + } + catch (SqliteException error) + { + throw new Exception($"Error while deleting coding session {sessionId} in database", error); + } + } + + internal static void UpdateRowInTable(int sessionId, string newTime, EditOptions editOption) + { + string updateSqlQuery = null; + + switch (editOption) + { + case EditOptions.EditStartTime: + updateSqlQuery = @"UPDATE codingSessions + SET StartTime = @newTime + WHERE Id = @sessionId"; + break; + case EditOptions.EditEndTime: + updateSqlQuery = @"UPDATE codingSessions + SET EndTime = @newTime + WHERE Id = @sessionId"; + break; + } + + try + { + _connection.Execute(updateSqlQuery, new { newTime, sessionId }); + } + catch(SqliteException error) + { + throw new Exception($"Error while updating coding session {sessionId} in database", error); + } + } +} \ No newline at end of file diff --git a/NoNameLudovik.CodingTracker/NoNameLudovik.CodingTracker/Enums.cs b/NoNameLudovik.CodingTracker/NoNameLudovik.CodingTracker/Enums.cs new file mode 100755 index 00000000..aa0efa23 --- /dev/null +++ b/NoNameLudovik.CodingTracker/NoNameLudovik.CodingTracker/Enums.cs @@ -0,0 +1,16 @@ +namespace NoNameLudovik.CodingTracker; + +internal enum MenuOptions +{ + AddSession, + EditSession, + DeleteSession, + ShowSessions, + Exit +} + +internal enum EditOptions +{ + EditStartTime, + EditEndTime, +} \ No newline at end of file diff --git a/NoNameLudovik.CodingTracker/NoNameLudovik.CodingTracker/Helper.cs b/NoNameLudovik.CodingTracker/NoNameLudovik.CodingTracker/Helper.cs new file mode 100644 index 00000000..138dfe1c --- /dev/null +++ b/NoNameLudovik.CodingTracker/NoNameLudovik.CodingTracker/Helper.cs @@ -0,0 +1,81 @@ +using System.Globalization; +using Spectre.Console; + +namespace NoNameLudovik.CodingTracker; + +internal static class Helper +{ + internal static DateTime GetDateTime() + { + while (true) + { + var input = AnsiConsole.Ask( + "Please, type in date at format \"dd-MM-yyyy HH:mm\" or leave it blank to type in [green]current time[/]", + DateTime.Now.ToString("dd-MM-yyyy HH:mm")); + + if (DateTime.TryParseExact(input, "dd-MM-yyyy HH:mm", new CultureInfo("en-US"), DateTimeStyles.None, out var date)) + return date; + + AnsiConsole.MarkupLine($"Wrong format [red]{input}[/]! Try again!"); + } + } + + internal static int GetId() + { + while (true) + { + var sessionId = AnsiConsole.Ask("Type in [green]ID[/] of session you want to edit?"); + if (!Helper.CheckIdExist(sessionId)) + { + AnsiConsole.MarkupLine($"[red]{sessionId}[/] doesn't exist! Try again!"); + continue; + } + + return sessionId; + } + } + + internal static bool TimeValidation(DateTime newTime, int sessionId, EditOptions option) + { + var session = DataBaseController.SelectFromTable(sessionId); + + switch (option) + { + case EditOptions.EditStartTime: + if (newTime < DateTime.ParseExact(session.EndTime, "dd-MM-yyyy HH:mm", new CultureInfo("en-US"), DateTimeStyles.None)) + { + return true; + } + break; + case EditOptions.EditEndTime: + if (newTime > DateTime.ParseExact(session.StartTime,"dd-MM-yyyy HH:mm", new CultureInfo("en-US"), DateTimeStyles.None)) + { + return true; + } + break; + } + + return false; + } + + internal static bool CompareDates(DateTime startTime, DateTime endTime) + { + if (startTime > endTime) + return false; + + return true; + } + + private static bool CheckIdExist(int sessionId) + { + var codingSessions = DataBaseController.SelectFromTable(); + var ids = new List(); + + foreach (var session in codingSessions) + { + ids.Add(session.Id); + } + + return ids.Contains(sessionId); + } +} \ No newline at end of file diff --git a/NoNameLudovik.CodingTracker/NoNameLudovik.CodingTracker/MenuController.cs b/NoNameLudovik.CodingTracker/NoNameLudovik.CodingTracker/MenuController.cs new file mode 100755 index 00000000..159a2fc1 --- /dev/null +++ b/NoNameLudovik.CodingTracker/NoNameLudovik.CodingTracker/MenuController.cs @@ -0,0 +1,152 @@ +using Spectre.Console; + +namespace NoNameLudovik.CodingTracker; + +internal class MenuController +{ + internal static void MainMenu() + { + while (true) + { + Console.Clear(); + + var optionChoice = AnsiConsole.Prompt( + new SelectionPrompt() + .Title("Choose an option:") + .AddChoices(Enum.GetValues())); + + switch (optionChoice) + { + case MenuOptions.AddSession: + AddSession(); + break; + case MenuOptions.EditSession: + EditSession(); + Console.ReadKey(); + break; + case MenuOptions.DeleteSession: + DeleteSession(); + Console.ReadKey(); + break; + case MenuOptions.ShowSessions: + ShowSessions(); + Console.ReadKey(); + break; + case MenuOptions.Exit: + Environment.Exit(0); + break; + } + } + } + + private static void AddSession() + { + while (true) + { + AnsiConsole.Clear(); + AnsiConsole.MarkupLine("[bold yellow]Add Session[/]\n"); + + AnsiConsole.MarkupLine("[blue]Start Time[/]"); + var startTime = Helper.GetDateTime(); + + AnsiConsole.MarkupLine("[blue]End Time[/]"); + var endTime = Helper.GetDateTime(); + + if (startTime > endTime) + { + AnsiConsole.MarkupLine("[red]StartTime[/] can't be later then [red]EndTime[/]!"); + Console.ReadKey(); + continue; + } + + try + { + DataBaseController.Insert(startTime, endTime); + AnsiConsole.MarkupLine("[green]Success![/]Session added!"); + } + catch (Exception error) + { + AnsiConsole.MarkupLine($"[red]{error.Message}[/]"); + } + + break; + } + } + + private static void EditSession() + { + ShowSessions(); + + DateTime newTime; + var sessionId = Helper.GetId(); + var editOption = AnsiConsole.Prompt(new SelectionPrompt(). + Title("Choose what you want to edit:"). + AddChoices(Enum.GetValues())); + + while (true) + { + AnsiConsole.Clear(); + newTime = Helper.GetDateTime(); + if (!Helper.TimeValidation(newTime, sessionId, editOption)) + { + AnsiConsole.MarkupLine("[red]StartTime[/] can't be later then [red]EndTime[/]!"); + Console.ReadKey(); + continue; + } + + break; + } + + try + { + DataBaseController.UpdateRowInTable(sessionId, newTime.ToString("dd-MM-yyyy HH:mm"), editOption); + + AnsiConsole.Clear(); + AnsiConsole.MarkupLine("[green]Success![/]Date was changed!"); + } + catch (Exception error) + { + AnsiConsole.MarkupLine($"[red]{error.Message}[/]"); + } + } + + private static void DeleteSession() + { + ShowSessions(); + + var sessionId = Helper.GetId(); + try + { + DataBaseController.DeleteFromTable(sessionId); + AnsiConsole.MarkupLine("[green]Success![/]Session deleted!"); + } + catch(Exception error) + { + AnsiConsole.MarkupLine($"[red]{error.Message}[/]"); + } + } + + private static void ShowSessions() + { + var codingSessions = DataBaseController.SelectFromTable(); + + var table = new Table(); + table.AddColumn(new TableColumn("[green]ID[/]").Centered()); + table.AddColumn(new TableColumn("[blue]StartTime[/]").Centered()); + table.AddColumn(new TableColumn("[blue]EndTime[/]").Centered()); + table.AddColumn(new TableColumn("[yellow]Duration[/]").Centered()); + table.ShowRowSeparators(); + table.Border(TableBorder.Rounded); + + foreach (var codingSession in codingSessions) + { + codingSession.CalculateDuration(); + table.AddRow(codingSession.Id.ToString(), + codingSession.StartTime, + codingSession.EndTime, + string.Format("{0}:{1:mm}:{1:ss}", (int)codingSession.Duration.TotalHours, codingSession.Duration)); + } + + AnsiConsole.Write(table); + } +} \ No newline at end of file diff --git a/NoNameLudovik.CodingTracker/NoNameLudovik.CodingTracker/NoNameLudovik.CodingTracker.csproj b/NoNameLudovik.CodingTracker/NoNameLudovik.CodingTracker/NoNameLudovik.CodingTracker.csproj new file mode 100755 index 00000000..ea76859e --- /dev/null +++ b/NoNameLudovik.CodingTracker/NoNameLudovik.CodingTracker/NoNameLudovik.CodingTracker.csproj @@ -0,0 +1,27 @@ + + + + Exe + net9.0 + enable + enable + + + + + + + + + + + + + + + + PreserveNewest + + + + diff --git a/NoNameLudovik.CodingTracker/NoNameLudovik.CodingTracker/Program.cs b/NoNameLudovik.CodingTracker/NoNameLudovik.CodingTracker/Program.cs new file mode 100755 index 00000000..5f770bc5 --- /dev/null +++ b/NoNameLudovik.CodingTracker/NoNameLudovik.CodingTracker/Program.cs @@ -0,0 +1,10 @@ +namespace NoNameLudovik.CodingTracker; + +class Program +{ + static void Main(string[] args) + { + DataBaseController.CreatTable(); + MenuController.MainMenu(); + } +} \ No newline at end of file diff --git a/NoNameLudovik.CodingTracker/NoNameLudovik.CodingTracker/appsettings.json b/NoNameLudovik.CodingTracker/NoNameLudovik.CodingTracker/appsettings.json new file mode 100644 index 00000000..938ea236 --- /dev/null +++ b/NoNameLudovik.CodingTracker/NoNameLudovik.CodingTracker/appsettings.json @@ -0,0 +1,4 @@ +{ + "connectionString": "Data Source=codingTrackerDB.db", + "dbPath": "codingTrackerDB.db" +} diff --git a/README.md b/README.md new file mode 100644 index 00000000..804b1401 --- /dev/null +++ b/README.md @@ -0,0 +1,102 @@ +# CodingTracker + +CodingTracker is a simple console-based application for tracking time spent coding. +The application is written in **C#** and uses **SQLite** for data storage. + +The main goal of this project is to practice working with databases, Dapper ORM, and building a clean console user interface using Spectre.Console. + +--- + +## Features + +- Add a new coding session (start time & end time) +- View all recorded coding sessions +- Update existing coding sessions +- Delete coding sessions +- Persistent data storage using SQLite +- User-friendly console UI with Spectre.Console + +--- + +## Technologies Used + +- **C#** +- **.NET** +- **SQLite** +- **Dapper ORM** +- **Spectre.Console** + +--- + +## Requirements + +- .NET SDK (recommended .NET 8 or newer) +- Windows, Linux, or macOS + +--- + +## Database + +The application uses SQLite as a local database. +The database file is created automatically on the first run of the application. + +## Project Purpose + +This project was created as a learning exercise to improve skills in: + +- C# and .NET +- Working with SQLite databases +- Using Dapper ORM +- Building interactive console applications +- Writing clean and maintainable code + +## Possible Future Improvements + +- Add daily / weekly / monthly statistics +- Add unit tests + +## How to Use + + **Run the Application** + Launch the console app — it will automatically create the database and table if need. + +**Main Menu** + + You will see the following option: + + 1. Add Session + + 2. Edit Session + + 3. Delete Session + + 4. Show Sessions + + 5. Exit + +**Add Session** +- Select option. +- Enter the start time (`dd-MM-yy HH:mm`). +- Enter the end time (`dd-MM-yy HH:mm`). + +**Edit Session** +- Select option. +- Type in the record `Id`. +- Provide a new start or end time. + +**Delete Session** +- Select option. +- Type in the record `Id`. + +**Show Sessions** +- Select this option to see all sessions. + + **Exit** +- Select to quit the application. + +## Lecense + +This project is open-source and available under the MIT License. + + +