diff --git a/CodingTracker.Myhos0/CodingSession.cs b/CodingTracker.Myhos0/CodingSession.cs new file mode 100644 index 000000000..5942771a9 --- /dev/null +++ b/CodingTracker.Myhos0/CodingSession.cs @@ -0,0 +1,11 @@ +namespace CodingTrackerProgram; + +internal class CodingSession +{ + public int Id { get; set; } + public DateTime Date { get; set; } + public string StartTime { get; set; } + public string EndTime { get; set; } + public string Duration { get; set; } +} + diff --git a/CodingTracker.Myhos0/CodingSessionRepository.cs b/CodingTracker.Myhos0/CodingSessionRepository.cs new file mode 100644 index 000000000..4971f99d5 --- /dev/null +++ b/CodingTracker.Myhos0/CodingSessionRepository.cs @@ -0,0 +1,77 @@ +using Dapper; + +namespace CodingTrackerProgram; + +internal class CodingSessionRepository +{ + private readonly DataBase _database; + + public CodingSessionRepository(DataBase dataBase) + { + _database = dataBase; + } + + public void Insert(CodingSession session) + { + const string SQL = @"INSERT INTO CodingSession(Date,StartTime,EndTime,Duration) Values(@Date,@StartTime,@EndTime,@Duration)"; + + using var connection = _database.GetConnection(); + connection.Execute(SQL, session); + } + + public IEnumerable GetAll() + { + const string SQL = @"SELECT Id,Date,StartTime,EndTime,Duration FROM CodingSession ORDER BY Date ASC"; + + using var connection = _database.GetConnection(); + return connection.Query(SQL); + } + + public void Delete(int id) + { + const string SQL = @"DElETE FROM CodingSession WHERE Id = @Id"; + + using var connection = _database.GetConnection(); + int rows = connection.Execute(SQL, new { Id = id }); + + if (rows == 0) + { + throw new Exception("No records Found."); + } + } + + public void Update(CodingSession session) + { + const string SQL = @"UPDATE CodingSession SET Date = @Date, StartTime = @StartTime, EndTime = @EndTime, Duration = @Duration WHERE Id = @Id"; + + using var connection = _database.GetConnection(); + int rows = connection.Execute(SQL, session); + + if (rows == 0) + throw new Exception("No record found to update."); + } + + public bool SessionExist(int id) + { + const string SQL = "SELECT 1 FROM CodingSession WHERE Id = @Id LIMIT 1"; + + using var connection = _database.GetConnection(); + return connection.ExecuteScalar(SQL, new { Id = id }) != null; + } + + public IEnumerable GetSessionsByDateRange(string start, string end) + { + const string SQL = @"SELECT * FROM CodingSession WHERE Date BETWEEN @Start AND @End ORDER BY Date ASC, Duration ASC;"; + + using var connection = _database.GetConnection(); + return connection.Query(SQL, new { Start = start, End = end }); + } + + public bool HasAnySessions() + { + const string SQL = "SELECT 1 FROM CodingSession LIMIT 1"; + + using var connection = _database.GetConnection(); + return connection.ExecuteScalar(SQL) != null; + } +} \ No newline at end of file diff --git a/CodingTracker.Myhos0/CodingSessionSeeder.cs b/CodingTracker.Myhos0/CodingSessionSeeder.cs new file mode 100644 index 000000000..46b03370a --- /dev/null +++ b/CodingTracker.Myhos0/CodingSessionSeeder.cs @@ -0,0 +1,48 @@ +using CodingTrackerProgram; +using Spectre.Console; +using System; + +namespace CodingTrackerProgram; + +internal class CodingSessionSeeder +{ + private readonly CodingSessionRepository repository; + private readonly Random random = new(); + + public CodingSessionSeeder(DataBase dataBase) + { + repository = new CodingSessionRepository(dataBase); + } + + public void Seed(int numberOfSessions) + { + for (int i = 0; i < numberOfSessions; i++) + { + CodingSession session = GenerateRandomSession(); + repository.Insert(session); + } + } + + private CodingSession GenerateRandomSession() + { + DateTime startRange = DateTime.Today.AddYears(-3); + int range = (DateTime.Today - startRange).Days; + + DateTime date = startRange.AddDays(random.Next(0, range)); + + TimeSpan start = TimeSpan.FromMinutes(random.Next(6 * 60, 22 * 60)); + + TimeSpan duration = TimeSpan.FromMinutes(random.Next(60,241)); + + TimeSpan end = start + duration; + + return new CodingSession + { + Date = date, + StartTime = start.ToString(@"hh\:mm\:ss"), + EndTime = end.ToString(@"hh\:mm\:ss"), + Duration = duration.ToString(@"hh\:mm\:ss") + }; + } +} + diff --git a/CodingTracker.Myhos0/CodingTracker.cs b/CodingTracker.Myhos0/CodingTracker.cs new file mode 100644 index 000000000..7231eef8f --- /dev/null +++ b/CodingTracker.Myhos0/CodingTracker.cs @@ -0,0 +1,383 @@ +using Spectre.Console; +using CodingTrackerProgram.Enums; +using System.Globalization; + +namespace CodingTrackerProgram; + +internal class CodingTracker +{ + private readonly CodingSession codingSession = new(); + private readonly CodingSessionRepository codingSessionRepository; + private readonly Stopwatch stopwatch; + private readonly ValidateInput validateInput = new(); + + public CodingTracker(DataBase dataBase) + { + codingSessionRepository = new CodingSessionRepository(dataBase); + stopwatch = new Stopwatch(dataBase); + } + + public void MainMenu() + { + bool open = true; + + while (open) + { + Console.Clear(); + + var menuOption = AnsiConsole.Prompt( + new SelectionPrompt() + .Title("Select the option") + .AddChoices(Enum.GetValues())); + + switch (menuOption) + { + case Menu.Stopwatch: + MainStopwatchMenu(); + break; + case Menu.ConsultMenu: + MainConsultMenu(); + break; + case Menu.Exit: + open = false; + break; + } + } + } + + public void MainStopwatchMenu() + { + while (true) + { + Console.Clear(); + + var menuOption = AnsiConsole.Prompt( + new SelectionPrompt() + .Title("Select the option") + .AddChoices(Enum.GetValues())); + + switch (menuOption) + { + case StopwatchMenu.Start: + stopwatch.StartTimer(); + break; + case StopwatchMenu.Exit: + return; + } + } + } + + public void MainConsultMenu() + { + while (true) + { + Console.Clear(); + + var menuOption = AnsiConsole.Prompt( + new SelectionPrompt() + .Title("Select the option") + .AddChoices(Enum.GetValues())); + + switch (menuOption) + { + case ConsultMenu.Insert: + InsertMenu(); + break; + case ConsultMenu.Update: + UpdateMenu(); + break; + case ConsultMenu.View: + ViewMenu(); + break; + case ConsultMenu.Delete: + DeleteMenu(); + break; + case ConsultMenu.PeriodMenu: + MainPeriodMenu(); + break; + case ConsultMenu.Exit: + return; + } + } + } + + public void MainPeriodMenu() + { + while (true) + { + Console.Clear(); + + var menuOption = AnsiConsole.Prompt + (new SelectionPrompt() + .Title("Select the option to show the filter") + .AddChoices(Enum.GetValues())); + + switch (menuOption) + { + case PeriodMenu.Day: + PerDayMenu(); + break; + case PeriodMenu.Week: + PerWeekMenu(); + break; + case PeriodMenu.Month: + PerMonthMenu(); + break; + case PeriodMenu.Year: + PerYearMenu(); + break; + case PeriodMenu.Cancel: + return; + } + } + } + + private void InsertMenu() + { + List values = GetUserInput( + "Please enter the date of the coding session [#619B8A](yyyy-MM-dd)[/], [#75C9C8]T[/] to insert today's date or [#9A031E]0[/] to cancel", + "Please enter the start time of the coding session [#619B8A](HH:mm:ss or HH:mm 00-23)[/], [#75C9C8]C[/] to insert current time or [#9A031E]0[/] to cancel", + "Please enter the end time of the coding session [#619B8A](HH:mm:ss or HH:mm 00-23)[/], [#75C9C8]C[/] to insert current time plus one hour or [#9A031E]0[/] to cancel" + ); + + if (values.Exists(v => v == "0")) return; + + DateTime parsedDate = DateTime.ParseExact(values[0], "yyyy-MM-dd", CultureInfo.InvariantCulture); + + CodingSession session = new() + { + Date = parsedDate, + StartTime = values[1], + EndTime = values[2], + Duration = values[3] + }; + + codingSessionRepository.Insert(session); + } + + private void ViewMenu() + { + IEnumerable sessions = codingSessionRepository.GetAll(); + + if (!sessions.Any()) + { + AnsiConsole.MarkupLine("[yellow]No coding sessions found.[/]"); + Console.ReadKey(); + return; + } + + ShowTable(sessions); + Console.ReadKey(); + } + + private void DeleteMenu() + { + ViewMenu(); + int id = validateInput.ValidateId("Select the [#619B8A]Id[/] of the session you want to delete or [#9A031E]0[/] to cancel", codingSessionRepository); + + if (id == -1) return; + + codingSessionRepository.Delete(id); + AnsiConsole.MarkupLine("Session delete successfully"); + Console.ReadLine(); + } + + private void UpdateMenu() + { + ViewMenu(); + int id = validateInput.ValidateId("Select the [#619B8A]Id[/] of the session you want to update or [#9A031E]0[/] to cancel", codingSessionRepository); + + if (id == -1) return; + + List values = GetUserInput( + "Please enter the new date of the coding session [#619B8A](yyyy-MM-dd)[/], [#75C9C8]T[/] to insert today's date or [#9A031E]0[/] to cancel", + "Please enter the new start time of the coding session [#619B8A](HH:mm:ss or HH:mm 00-23)[/], [#75C9C8]C[/] to insert current time or [#9A031E]0[/] to cancel", + "Please enter the new end time of the coding session [#619B8A](HH:mm:ss or HH:mm 00-23)[/], [#75C9C8]C[/] to insert current time plus one hour or [#9A031E]0[/] to cancel" + ); + + if (values.Exists(v => v == "0")) return; + + DateTime parsedDate = DateTime.ParseExact(values[0], "yyyy-MM-dd", CultureInfo.InvariantCulture); + + CodingSession session = new() + { + Id = id, + Date = parsedDate, + StartTime = values[1], + EndTime = values[2], + Duration = values[3] + }; + + codingSessionRepository.Update(session); + + } + + private List GetUserInput(string dateMessage, string startTimeMessage, string endTimeMessage) + { + List values = new(); + + string minimumDuration = TimeSpan.FromHours(1).ToString(); + + string date = validateInput.ValidateDate(dateMessage); + string startTime = validateInput.ValidateTime(startTimeMessage); + string endTime = validateInput.ValidateEndTime(startTime, endTimeMessage,minimumDuration); + + TimeSpan calculateDuration = TimeSpan.Parse(endTime) - TimeSpan.Parse(startTime); + + string duration = calculateDuration.ToString(); + + values.Add(date); + values.Add(startTime); + values.Add(endTime); + values.Add(duration); + + AnsiConsole.MarkupLine($"Date: {date}"); + AnsiConsole.MarkupLine($"Star Time: {startTime}"); + AnsiConsole.MarkupLine($"End Time: {endTime}"); + AnsiConsole.MarkupLine($"Duration: {duration}"); + Console.ReadKey(); + + return values; + } + + private void PerDayMenu() + { + string date = validateInput.ValidateDate("Please enter a date [#619B8A](yyyy-MM-dd)[/]"); + + DateTime start = DateTime.Parse(date); + DateTime end = start.AddDays(1); + + IEnumerable sessions = codingSessionRepository.GetSessionsByDateRange(start.ToString("yyyy-MM-dd"), end.ToString("yyyy-MM-dd")); + + if (!sessions.Any()) + { + AnsiConsole.MarkupLine("[yellow]No coding sessions found.[/]"); + Console.ReadKey(); + return; + } + + ShowTable(sessions); + Console.ReadKey(); + } + + private void PerWeekMenu() + { + int year = validateInput.ValidateYear("Enter the year [#619B8A](yyyy)[/]"); + int month = validateInput.ValidateMonth("Enter the month [#619B8A]1-12[/]"); + int week = validateInput.ValidateWeekInMonth(GetWeeksInMonth(year,month)); + + var (starWeek, endWeek) = GetWeekDateRange(year,month,week); + + IEnumerable sessions = codingSessionRepository.GetSessionsByDateRange(starWeek.ToString("yyyy-MM-dd"), endWeek.AddDays(1).ToString("yyyy-MM-dd")); + + if (!sessions.Any()) + { + AnsiConsole.MarkupLine("[yellow]No coding sessions found.[/]"); + Console.ReadKey(); + return; + } + + ShowTable(sessions); + Console.ReadKey(); + } + + private void PerMonthMenu() + { + int year = validateInput.ValidateYear("Enter the year [#619B8A](yyyy)[/]"); + int month = validateInput.ValidateMonth("Enter the month [#619B8A]1-12[/]"); + + DateTime startMonth = new(year, month, 1); + DateTime endMonth = startMonth.AddMonths(1); + + IEnumerable sessions = codingSessionRepository.GetSessionsByDateRange(startMonth.ToString("yyyy-MM-dd"),endMonth.ToString("yyyy-MM-dd")); + + if (!sessions.Any()) + { + AnsiConsole.MarkupLine("[yellow]No coding sessions found.[/]"); + Console.ReadKey(); + return; + } + + ShowTable(sessions); + Console.ReadKey(); + } + + private void PerYearMenu() + { + int year = validateInput.ValidateYear("Enter the year [#619B8A](yyyy)[/]"); + + DateTime start = new(year, 1, 1); + DateTime end = start.AddYears(1); + + IEnumerable sessions = codingSessionRepository.GetSessionsByDateRange(start.ToString("yyyy-MM-dd"), end.ToString("yyyy-MM-dd")); + if (!sessions.Any()) + { + AnsiConsole.MarkupLine("[yellow]No coding sessions found.[/]"); + Console.ReadKey(); + return; + } + + ShowTable(sessions); + Console.ReadKey(); + } + + private void ShowTable(IEnumerable sessions) + { + AnsiConsole.Clear(); + + var table = new Table().Border(TableBorder.Rounded) + .AddColumn("Id") + .AddColumn("Date") + .AddColumn("Start") + .AddColumn("End") + .AddColumn("Duration"); + + foreach (var s in sessions) + { + table.AddRow( + s.Id.ToString(), + s.Date.ToString("yyyy-MM-dd"), + TimeSpan.Parse(s.StartTime).ToString(@"hh\:mm\:ss"), + TimeSpan.Parse(s.EndTime).ToString(@"hh\:mm\:ss"), + TimeSpan.Parse(s.Duration).ToString(@"hh\:mm\:ss") + ); + } + + table.Expand(); + AnsiConsole.Write(table); + } + + private int GetWeeksInMonth(int year, int month) + { + DateTime firstDay = new(year, month, 1); + DateTime lastDay = firstDay.AddMonths(1).AddDays(-1); + + int firstWeek = CultureInfo.InvariantCulture.Calendar.GetWeekOfYear( + firstDay, + CalendarWeekRule.FirstFourDayWeek, + DayOfWeek.Monday); + + int lastWeek = CultureInfo.InvariantCulture.Calendar.GetWeekOfYear( + lastDay, + CalendarWeekRule.FirstFourDayWeek, + DayOfWeek.Monday); + + if (lastWeek < firstWeek) + lastWeek += 52; + + return lastWeek - firstWeek + 1; + } + + private (DateTime Start, DateTime End) GetWeekDateRange(int year, int month, int week) + { + DateTime firstDayOfMonth = new(year, month, 1); + + while (firstDayOfMonth.DayOfWeek != DayOfWeek.Monday) + firstDayOfMonth = firstDayOfMonth.AddDays(1); + + DateTime start = firstDayOfMonth.AddDays((week - 1) * 7); + DateTime end = start.AddDays(6); + + return (start, end); + } +} \ No newline at end of file diff --git a/CodingTracker.Myhos0/CodingTracker.csproj b/CodingTracker.Myhos0/CodingTracker.csproj new file mode 100644 index 000000000..d41735cbe --- /dev/null +++ b/CodingTracker.Myhos0/CodingTracker.csproj @@ -0,0 +1,24 @@ + + + + Exe + net9.0 + enable + enable + + + + + + + + + + + + + PreserveNewest + + + + diff --git a/CodingTracker.Myhos0/CodingTracker.sln b/CodingTracker.Myhos0/CodingTracker.sln new file mode 100644 index 000000000..b557bad1d --- /dev/null +++ b/CodingTracker.Myhos0/CodingTracker.sln @@ -0,0 +1,25 @@ + +Microsoft Visual Studio Solution File, Format Version 12.00 +# Visual Studio Version 17 +VisualStudioVersion = 17.14.36915.13 d17.14 +MinimumVisualStudioVersion = 10.0.40219.1 +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "CodingTracker", "CodingTracker.csproj", "{D1093329-212F-4AD4-8E23-F9DF267975BE}" +EndProject +Global + GlobalSection(SolutionConfigurationPlatforms) = preSolution + Debug|Any CPU = Debug|Any CPU + Release|Any CPU = Release|Any CPU + EndGlobalSection + GlobalSection(ProjectConfigurationPlatforms) = postSolution + {D1093329-212F-4AD4-8E23-F9DF267975BE}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {D1093329-212F-4AD4-8E23-F9DF267975BE}.Debug|Any CPU.Build.0 = Debug|Any CPU + {D1093329-212F-4AD4-8E23-F9DF267975BE}.Release|Any CPU.ActiveCfg = Release|Any CPU + {D1093329-212F-4AD4-8E23-F9DF267975BE}.Release|Any CPU.Build.0 = Release|Any CPU + EndGlobalSection + GlobalSection(SolutionProperties) = preSolution + HideSolutionNode = FALSE + EndGlobalSection + GlobalSection(ExtensibilityGlobals) = postSolution + SolutionGuid = {A0DDC3C3-3173-45C0-9928-D4CDAFE4E300} + EndGlobalSection +EndGlobal diff --git a/CodingTracker.Myhos0/DataBase.cs b/CodingTracker.Myhos0/DataBase.cs new file mode 100644 index 000000000..900fd47fa --- /dev/null +++ b/CodingTracker.Myhos0/DataBase.cs @@ -0,0 +1,34 @@ +using Microsoft.Data.Sqlite; +using Microsoft.Extensions.Configuration; + +namespace CodingTrackerProgram; + +internal class DataBase +{ + private readonly string _connectionString; + + public DataBase(string connectionString) + { + _connectionString = connectionString; + } + + public SqliteConnection GetConnection() => new(_connectionString); + + internal void CreateTable() + { + using var connection = GetConnection(); + + connection.Open(); + + var tableCmd = connection.CreateCommand(); + tableCmd.CommandText = + @"CREATE TABLE IF NOT EXISTS CodingSession( + Id INTEGER PRIMARY KEY AUTOINCREMENT, + Date TEXT NOT NULL, + StartTime TEXT NOT NULL, + EndTime TEXT NOT NULL, + Duration TEXT NOT NULL)"; + + tableCmd.ExecuteNonQuery(); + } +} \ No newline at end of file diff --git a/CodingTracker.Myhos0/Enum.cs b/CodingTracker.Myhos0/Enum.cs new file mode 100644 index 000000000..a1cea0fed --- /dev/null +++ b/CodingTracker.Myhos0/Enum.cs @@ -0,0 +1,32 @@ +namespace CodingTrackerProgram.Enums; + +internal enum ConsultMenu +{ + Insert, + Update, + Delete, + View, + PeriodMenu, + Exit +} + +internal enum StopwatchMenu +{ + Start, + Exit +} +internal enum Menu +{ + Stopwatch, + ConsultMenu, + Exit +} + +internal enum PeriodMenu +{ + Day, + Week, + Month, + Year, + Cancel +} \ No newline at end of file diff --git a/CodingTracker.Myhos0/Program.cs b/CodingTracker.Myhos0/Program.cs new file mode 100644 index 000000000..584ceb100 --- /dev/null +++ b/CodingTracker.Myhos0/Program.cs @@ -0,0 +1,35 @@ +using CodingTrackerProgram; +using Microsoft.Extensions.Configuration; +using Spectre.Console; + +class Program +{ + private static CodingTracker? codingTracker; + private static CodingTrackerProgram.Stopwatch? stopwatch; + public static void Main() + { + var configuration = new ConfigurationBuilder() + .SetBasePath(Directory.GetCurrentDirectory()) + .AddJsonFile("appsettings.json", optional: false) + .Build(); + + string connectionString = configuration.GetConnectionString("DefaultConnection"); + + DataBase db = new(connectionString); + db.CreateTable(); + + var repository = new CodingSessionRepository(db); + + if (!repository.HasAnySessions()) + { + var seeder = new CodingSessionSeeder(db); + seeder.Seed(100); + } + + codingTracker = new CodingTracker(db); + + codingTracker.MainMenu(); + + Environment.Exit(0); + } +} \ No newline at end of file diff --git a/CodingTracker.Myhos0/README.md b/CodingTracker.Myhos0/README.md new file mode 100644 index 000000000..12bf9393c --- /dev/null +++ b/CodingTracker.Myhos0/README.md @@ -0,0 +1,103 @@ +# CodingTracker +My fourth console project at C# Academy. +A console application where we track code sessions by how long they lasted. +# Requirements + +- [x] This application has the same requirements as the previous project, except that now you'll be logging your daily coding time. +- [x] To show the data on the console, you should use the Spectre.Console library. +- [x] You're required to have separate classes in different files (i.e. UserInput.cs, Validation.cs, CodingController.cs) +- [x] You should tell the user the specific format you want the date and time to be logged and not allow any other format. +- [x] You'll need to create a configuration file called appsettings.json, which will contain your database path and connection strings (and any other configs you might need). +- [x] You'll need to create a CodingSession class in a separate file. It will contain the properties of your coding session: Id, StartTime, EndTime, Duration. When reading from the database, you can't use an anonymous object, you have to read your table into a List of CodingSession. +- [x] The user shouldn't input the duration of the session. It should be calculated based on the Start and End times +- [x] The user should be able to input the start and end times manually. +- [x] You need to use Dapper ORM for the data access instead of ADO.NET. (This requirement was included in Feb/2024) +- [x] Follow the DRY Principle, and avoid code repetition. +- [x] Don't forget the ReadMe explaining your thought process. + +# Features + +- **SQLite database connection** + - The program uses SQLite to create a database to store and read information. + - When the program is started, the application generates the database and table necessary for its operation. + - When the application is launched, the program inserts 100 test values (2023–2026). + +- **Spectre Console UI** + - The application uses a Spectre Console UI where we can navigate using the keyboard. + +

+ Main menu +

+ + - The first menu shows two options: + - Stopwatch + - ConsultMenu +## Stopwatch + +- The Stopwatch menu allows us to time the duration of a coding session and stop with the key ENTER, the coding session is saved when the stopwatch stops or the aplication is closes. +- The duration of the session must be greater than five minutes to save in both cases. +- **Examples:** + +

+ imagen +

+ +

+ imagen +

+ +## ConsultMenu + +

+ Consult menu +

+ +- The consult menu shows basic CRUD operations and a fifth option to perform filtered queries. + +### PeriodMenu + +

+ Period menu +

+ +- The period menu shows five options to perform different queries: + - Filter by day, week, month, and year + - Results are ordered by: + - Date (ascending) + - Duration (ascending) + +- **Example** + +

+ Example result +

+ +# Chanllenges +- One of the difficulties I encountered in completing this project was implementing the stopwatch, mainly due to two factors: + - Display the stopwatch correctly on the live screen, spectre console made this point easy. + - That the session will be saved correctly when the stopwatch is stopped or the application is closed abruptly. +- Add filtering of sessions by date, mainly by week, as we need to learn how Sqlite stores dates in order to filter them. +- Working with times and dates was complicated at first, but once I understood how TimeSpan and DateTime objects work, it became clearer how to implement them. + +# What I have learned +- How TimeSpan and DateTime work in C# and their methods for performing different operations. +- Add a repository to query the database. +- Use Spectre Console to display live information that is constantly updated. +- How to handle unexpected application closure without losing information. +- Add more details to the application to make it look better. + +# Areas to improve +- Improve the way classes are organised to make them easier to understand, use folders to improve structure. +- Implement better methods, objects, or classes that help control exceptions if the application is forced to close. +- Learn more about how to write SQL queries that are better suited to what I want to do. +- How to correctly configure the database settings from a .json file and locate the database in a project folder of my choice. + - For example, having a folder where the database is stored and that folder is in the root of the project where the .cs classes are stored. (I don't know if this is good practice.) + +# Resourced Used +- C# Separation of Concerns article: https://www.thecsharpacademy.com/article/30005/separation-of-concerns-csharp +- Specter Console Documentation: https://spectreconsole.net/ +- Learn Dapper: https://www.learndapper.com/ +- SQlite Web: https://www.sqlite.org/ +- Microsoft documentation for DateTime: https://learn.microsoft.com/en-us/dotnet/api/system.datetime?view=net-10.0 +- Microsoft documentation for TimeSpan: https://learn.microsoft.com/es-es/dotnet/api/system.timespan?view=net-8.0 +- Microsoft documentation for IEnumerable https://learn.microsoft.com/es-es/dotnet/api/system.collections.ienumerable?view=net-8.0 diff --git a/CodingTracker.Myhos0/Stopwatch.cs b/CodingTracker.Myhos0/Stopwatch.cs new file mode 100644 index 000000000..5e904834d --- /dev/null +++ b/CodingTracker.Myhos0/Stopwatch.cs @@ -0,0 +1,144 @@ +using CodingTrackerProgram; +using Spectre.Console; + +namespace CodingTrackerProgram; + +internal class Stopwatch +{ + private readonly CodingSessionRepository codingSessionRepository; + private DateTime? startDateTime; + private bool running; + private bool alreadySaved; + + public Stopwatch(DataBase dataBase) + { + codingSessionRepository = new CodingSessionRepository(dataBase); + Console.CancelKeyPress += OnCancelKeyPress; + AppDomain.CurrentDomain.ProcessExit += OnProcessExit; + } + + public void StartTimer() + { + Console.Clear(); + + startDateTime = DateTime.Now; + running = true; + + AnsiConsole.MarkupLine("[green]Coding session started...[/]"); + AnsiConsole.MarkupLine("\nPress [yellow]ENTER[/] to stop."); + + AnsiConsole.Live(CreatePanel(TimeSpan.Zero)) + .AutoClear(false) + .Start(ctx => + { + while (running) + { + var elapsed = DateTime.Now - startDateTime!.Value; + + ctx.UpdateTarget(CreatePanel(elapsed)); + + ctx.Refresh(); + + if (Console.KeyAvailable && + Console.ReadKey(true).Key == ConsoleKey.Enter) + { + alreadySaved = false; + StopAndSave(); + return; + } + + Thread.Sleep(1000); + } + }); + } + + private static Panel CreatePanel(TimeSpan elapsed) + { + return new Panel( + new Markup($"[bold yellow]{elapsed:hh\\:mm\\:ss}[/]").Centered()) + { + Border = BoxBorder.Rounded, + Header = new PanelHeader("Stopwatch", Justify.Center), + Padding = new Padding(20,1), + Expand = true, + }; + } + + + + public void StopAndSave() + { + if (alreadySaved) return; + alreadySaved = true; + + running = false; + + DateTime endTime = DateTime.Now; + TimeSpan duration = endTime - startDateTime.Value; + + if (duration < TimeSpan.FromMinutes(5)) + { + Console.Clear(); + AnsiConsole.MarkupLine("\n[red]Session too short. Not saved.[/]"); + Console.ReadKey(); + return; + } + + codingSessionRepository.Insert(new CodingSession + { + Date = startDateTime.Value, + StartTime = startDateTime.Value.ToString(@"hh\:mm\:ss"), + EndTime = endTime.ToString(@"hh\:mm\:ss"), + Duration = duration.ToString(@"hh\:mm\:ss") + + }); + + Console.Clear(); + AnsiConsole.MarkupLine($"[green]Session saved:[/]"); + Console.ReadKey(); + + } + + public void StopAndSaveAndExit() + { + if (alreadySaved) return; + alreadySaved = true; + + running = false; + + if (startDateTime != null) + { + DateTime endTime = DateTime.Now; + TimeSpan duration = endTime - startDateTime.Value; + + if (duration < TimeSpan.FromMinutes(5)) + { + Console.Clear(); + AnsiConsole.MarkupLine("\n[red]Session too short. Not saved.[/]"); + Console.ReadKey(); + return; + } + + codingSessionRepository.Insert(new CodingSession + { + Date = startDateTime.Value, + StartTime = startDateTime.Value.ToString(@"hh\:mm\:ss"), + EndTime = endTime.ToString(@"hh\:mm\:ss"), + Duration = duration.ToString(@"hh\:mm\:ss") + }); + } + } + + private void OnCancelKeyPress(object? sender, ConsoleCancelEventArgs e) + { + alreadySaved = false; + StopAndSaveAndExit(); + Console.Clear(); + Environment.Exit(0); + } + + private void OnProcessExit(object? sender, EventArgs e) + { + StopAndSaveAndExit(); + } +} \ No newline at end of file diff --git a/CodingTracker.Myhos0/ValidateInput.cs b/CodingTracker.Myhos0/ValidateInput.cs new file mode 100644 index 000000000..07f7f7b60 --- /dev/null +++ b/CodingTracker.Myhos0/ValidateInput.cs @@ -0,0 +1,172 @@ +using Spectre.Console; +using System.Globalization; + +namespace CodingTrackerProgram; + +internal class ValidateInput +{ + internal string ValidateDate(string message) + { + while (true) + { + AnsiConsole.MarkupLine(message); + string input = Console.ReadLine()?.Trim() ?? ""; + + if (string.Equals(input, "T", StringComparison.OrdinalIgnoreCase)) + { + return DateTime.Today.ToString("yyyy-MM-dd"); + } + + if (input == "0") return "0"; + + if (DateTime.TryParseExact(input, "yyyy-MM-dd", CultureInfo.InvariantCulture, DateTimeStyles.None, out var parsed)) + { + return parsed.ToString("yyyy-MM-dd"); + } + + message = "[#FF0A0A]Invalid date.[/] Try again:"; + } + } + + internal string ValidateTime(string message) + { + string[] timeFormat = { @"hh\:mm", @"hh\:mm\:ss" }; + + while (true) + { + AnsiConsole.MarkupLine(message); + string input = Console.ReadLine()?.Trim() ?? ""; + + if (string.Equals(input, "C", StringComparison.OrdinalIgnoreCase)) + { + return DateTime.Now.ToString(@"hh\:mm\:ss"); + } + + if (input == "0") return "0"; + + if (TimeSpan.TryParseExact(input, timeFormat, CultureInfo.InvariantCulture, out var parsed)) + { + return parsed.ToString(@"hh\:mm\:ss"); + } + + message = "[#FF0A0A]Invalid time.[/] Try again:"; + } + } + + internal int ValidateId(string message, CodingSessionRepository codingSessionRepository) + { + while (true) + { + AnsiConsole.MarkupLine(message); + string id = Console.ReadLine()?.Trim() ?? ""; + + if (id == "0") return -1; + + if (int.TryParse(id, out var parsed)) + { + if (codingSessionRepository.SessionExist(parsed)) + { + return parsed; + } + } + + message = "[#FF0A0A]Invalid Id.[/] Try again:"; + } + } + + internal string ValidateEndTime(string startTime, string endTimeMessage, string minimumDuration) + { + while (true) + { + string endTime = ValidateTime(endTimeMessage); + + TimeSpan duration = TimeSpan.Parse(endTime) - TimeSpan.Parse(startTime); + + + if (duration <= TimeSpan.Parse(minimumDuration)) + { + TimeSpan newEndTime = TimeSpan.Parse(endTime) + TimeSpan.Parse(minimumDuration); + + return newEndTime.ToString(); + } + + endTimeMessage = "The end time cannot be the same as or earlier than the start time."; + } + } + + public int ValidateWeekInMonth(int maxWeeks) + { + int week; + + while (true) + { + AnsiConsole.MarkupLine($"Enter week of the month [green](1 - {maxWeeks})[/]:"); + + string input = Console.ReadLine()?.Trim() ?? ""; + + if (!int.TryParse(input, out week)) + { + AnsiConsole.MarkupLine("[red]Week must be a number.[/]"); + continue; + } + + if (week < 1 || week > maxWeeks) + { + AnsiConsole.MarkupLine($"[red]Week must be between 1 and {maxWeeks}.[/]"); + continue; + } + + return week; + } + } + + public int ValidateMonth(string message) + { + int month; + + while (true) + { + AnsiConsole.MarkupLine(message); + string input = Console.ReadLine()?.Trim() ?? ""; + + if (!int.TryParse(input, out month)) + { + AnsiConsole.MarkupLine("[red]Month must be a number.[/]"); + continue; + } + + if (month < 1 || month > 12) + { + AnsiConsole.MarkupLine("[red]Month must be between 1 and 12.[/]"); + continue; + } + + return month; + } + } + + public int ValidateYear(string message) + { + int year; + + while (true) + { + AnsiConsole.MarkupLine(message); + string input = Console.ReadLine()?.Trim() ?? ""; + + if (!int.TryParse(input, out year)) + { + AnsiConsole.MarkupLine("[red]Year must be a number.[/]"); + continue; + } + + if (year < 1900 || year > DateTime.Now.Year + 1) + { + AnsiConsole.MarkupLine($"[red]Year must be between 1900 and {DateTime.Now.Year + 1}.[/]"); + continue; + } + + return year; + } + } +} diff --git a/CodingTracker.Myhos0/appsettings.json b/CodingTracker.Myhos0/appsettings.json new file mode 100644 index 000000000..a1c326c04 --- /dev/null +++ b/CodingTracker.Myhos0/appsettings.json @@ -0,0 +1,5 @@ +{ + "ConnectionStrings": { + "DefaultConnection": "Data Source=DataBase.db;" + } +}