diff --git a/STUDY.CodingTracker.Sephydev/.gitattributes b/STUDY.CodingTracker.Sephydev/.gitattributes new file mode 100644 index 000000000..1ff0c4230 --- /dev/null +++ b/STUDY.CodingTracker.Sephydev/.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/STUDY.CodingTracker.Sephydev/.gitignore b/STUDY.CodingTracker.Sephydev/.gitignore new file mode 100644 index 000000000..9491a2fda --- /dev/null +++ b/STUDY.CodingTracker.Sephydev/.gitignore @@ -0,0 +1,363 @@ +## Ignore Visual Studio temporary files, build results, and +## files generated by popular Visual Studio add-ons. +## +## Get latest from https://github.com/github/gitignore/blob/master/VisualStudio.gitignore + +# User-specific files +*.rsuser +*.suo +*.user +*.userosscache +*.sln.docstates + +# User-specific files (MonoDevelop/Xamarin Studio) +*.userprefs + +# Mono auto generated files +mono_crash.* + +# Build results +[Dd]ebug/ +[Dd]ebugPublic/ +[Rr]elease/ +[Rr]eleases/ +x64/ +x86/ +[Ww][Ii][Nn]32/ +[Aa][Rr][Mm]/ +[Aa][Rr][Mm]64/ +bld/ +[Bb]in/ +[Oo]bj/ +[Oo]ut/ +[Ll]og/ +[Ll]ogs/ + +# Visual Studio 2015/2017 cache/options directory +.vs/ +# Uncomment if you have tasks that create the project's static files in wwwroot +#wwwroot/ + +# Visual Studio 2017 auto generated files +Generated\ Files/ + +# MSTest test Results +[Tt]est[Rr]esult*/ +[Bb]uild[Ll]og.* + +# NUnit +*.VisualState.xml +TestResult.xml +nunit-*.xml + +# Build Results of an ATL Project +[Dd]ebugPS/ +[Rr]eleasePS/ +dlldata.c + +# Benchmark Results +BenchmarkDotNet.Artifacts/ + +# .NET Core +project.lock.json +project.fragment.lock.json +artifacts/ + +# ASP.NET Scaffolding +ScaffoldingReadMe.txt + +# StyleCop +StyleCopReport.xml + +# Files built by Visual Studio +*_i.c +*_p.c +*_h.h +*.ilk +*.meta +*.obj +*.iobj +*.pch +*.pdb +*.ipdb +*.pgc +*.pgd +*.rsp +*.sbr +*.tlb +*.tli +*.tlh +*.tmp +*.tmp_proj +*_wpftmp.csproj +*.log +*.vspscc +*.vssscc +.builds +*.pidb +*.svclog +*.scc + +# Chutzpah Test files +_Chutzpah* + +# Visual C++ cache files +ipch/ +*.aps +*.ncb +*.opendb +*.opensdf +*.sdf +*.cachefile +*.VC.db +*.VC.VC.opendb + +# Visual Studio profiler +*.psess +*.vsp +*.vspx +*.sap + +# Visual Studio Trace Files +*.e2e + +# TFS 2012 Local Workspace +$tf/ + +# Guidance Automation Toolkit +*.gpState + +# ReSharper is a .NET coding add-in +_ReSharper*/ +*.[Rr]e[Ss]harper +*.DotSettings.user + +# TeamCity is a build add-in +_TeamCity* + +# DotCover is a Code Coverage Tool +*.dotCover + +# AxoCover is a Code Coverage Tool +.axoCover/* +!.axoCover/settings.json + +# Coverlet is a free, cross platform Code Coverage Tool +coverage*.json +coverage*.xml +coverage*.info + +# Visual Studio code coverage results +*.coverage +*.coveragexml + +# NCrunch +_NCrunch_* +.*crunch*.local.xml +nCrunchTemp_* + +# MightyMoose +*.mm.* +AutoTest.Net/ + +# Web workbench (sass) +.sass-cache/ + +# Installshield output folder +[Ee]xpress/ + +# DocProject is a documentation generator add-in +DocProject/buildhelp/ +DocProject/Help/*.HxT +DocProject/Help/*.HxC +DocProject/Help/*.hhc +DocProject/Help/*.hhk +DocProject/Help/*.hhp +DocProject/Help/Html2 +DocProject/Help/html + +# Click-Once directory +publish/ + +# Publish Web Output +*.[Pp]ublish.xml +*.azurePubxml +# Note: Comment the next line if you want to checkin your web deploy settings, +# but database connection strings (with potential passwords) will be unencrypted +*.pubxml +*.publishproj + +# Microsoft Azure Web App publish settings. Comment the next line if you want to +# checkin your Azure Web App publish settings, but sensitive information contained +# in these scripts will be unencrypted +PublishScripts/ + +# NuGet Packages +*.nupkg +# NuGet Symbol Packages +*.snupkg +# The packages folder can be ignored because of Package Restore +**/[Pp]ackages/* +# except build/, which is used as an MSBuild target. +!**/[Pp]ackages/build/ +# Uncomment if necessary however generally it will be regenerated when needed +#!**/[Pp]ackages/repositories.config +# NuGet v3's project.json files produces more ignorable files +*.nuget.props +*.nuget.targets + +# Microsoft Azure Build Output +csx/ +*.build.csdef + +# Microsoft Azure Emulator +ecf/ +rcf/ + +# Windows Store app package directories and files +AppPackages/ +BundleArtifacts/ +Package.StoreAssociation.xml +_pkginfo.txt +*.appx +*.appxbundle +*.appxupload + +# Visual Studio cache files +# files ending in .cache can be ignored +*.[Cc]ache +# but keep track of directories ending in .cache +!?*.[Cc]ache/ + +# Others +ClientBin/ +~$* +*~ +*.dbmdl +*.dbproj.schemaview +*.jfm +*.pfx +*.publishsettings +orleans.codegen.cs + +# Including strong name files can present a security risk +# (https://github.com/github/gitignore/pull/2483#issue-259490424) +#*.snk + +# Since there are multiple workflows, uncomment next line to ignore bower_components +# (https://github.com/github/gitignore/pull/1529#issuecomment-104372622) +#bower_components/ + +# RIA/Silverlight projects +Generated_Code/ + +# Backup & report files from converting an old project file +# to a newer Visual Studio version. Backup files are not needed, +# because we have git ;-) +_UpgradeReport_Files/ +Backup*/ +UpgradeLog*.XML +UpgradeLog*.htm +ServiceFabricBackup/ +*.rptproj.bak + +# SQL Server files +*.mdf +*.ldf +*.ndf + +# Business Intelligence projects +*.rdl.data +*.bim.layout +*.bim_*.settings +*.rptproj.rsuser +*- [Bb]ackup.rdl +*- [Bb]ackup ([0-9]).rdl +*- [Bb]ackup ([0-9][0-9]).rdl + +# Microsoft Fakes +FakesAssemblies/ + +# GhostDoc plugin setting file +*.GhostDoc.xml + +# Node.js Tools for Visual Studio +.ntvs_analysis.dat +node_modules/ + +# Visual Studio 6 build log +*.plg + +# Visual Studio 6 workspace options file +*.opt + +# Visual Studio 6 auto-generated workspace file (contains which files were open etc.) +*.vbw + +# Visual Studio LightSwitch build output +**/*.HTMLClient/GeneratedArtifacts +**/*.DesktopClient/GeneratedArtifacts +**/*.DesktopClient/ModelManifest.xml +**/*.Server/GeneratedArtifacts +**/*.Server/ModelManifest.xml +_Pvt_Extensions + +# Paket dependency manager +.paket/paket.exe +paket-files/ + +# FAKE - F# Make +.fake/ + +# CodeRush personal settings +.cr/personal + +# Python Tools for Visual Studio (PTVS) +__pycache__/ +*.pyc + +# Cake - Uncomment if you are using it +# tools/** +# !tools/packages.config + +# Tabs Studio +*.tss + +# Telerik's JustMock configuration file +*.jmconfig + +# BizTalk build output +*.btp.cs +*.btm.cs +*.odx.cs +*.xsd.cs + +# OpenCover UI analysis results +OpenCover/ + +# Azure Stream Analytics local run output +ASALocalRun/ + +# MSBuild Binary and Structured Log +*.binlog + +# NVidia Nsight GPU debugger configuration file +*.nvuser + +# MFractors (Xamarin productivity tool) working folder +.mfractor/ + +# Local History for Visual Studio +.localhistory/ + +# BeatPulse healthcheck temp database +healthchecksdb + +# Backup folder for Package Reference Convert tool in Visual Studio 2017 +MigrationBackup/ + +# Ionide (cross platform F# VS Code tools) working folder +.ionide/ + +# Fody - auto-generated XML schema +FodyWeavers.xsd \ No newline at end of file diff --git a/STUDY.CodingTracker.Sephydev/README.md b/STUDY.CodingTracker.Sephydev/README.md new file mode 100644 index 000000000..66a1ffb6a --- /dev/null +++ b/STUDY.CodingTracker.Sephydev/README.md @@ -0,0 +1,70 @@ +# Coding Tracker + +My first C# console application using Object Oriented Programming, Dapper and Spectre.Console. + +Console based CRUD application to track occurrences of different coding sessions. Developed using C#, SQLite and NUnit. + +## Given Requirements + - This application has the same requirements as the previous project, except that now you'll be logging your daily coding time. + - To show the data on the console, you should use the Spectre.Console library. + - You're required to have separate classes in different files (i.e. UserInput.cs, Validation.cs, CodingController.cs) + - You should tell the user the specific format you want the date and time to be logged and not allow any other format. + - 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). + - 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. + - The user shouldn't input the duration of the session. It should be calculated based on the Start and End times + - The user should be able to input the start and end times manually. + - You need to use Dapper ORM for the data access instead of ADO.NET. + - Follow the DRY Principle, and avoid code repetition. + - Your project needs to contain a ReadMe file where you'll explain how your app works and tell a little about your thought progress. + +## Optional Challenging Requirements + - Add the possibility of tracking the coding time via a stopwatch so the user can track the session as it happens. + - Let the users filter their coding records per period (weeks, days, years) and/or order ascending or descending. + - If you already have a bit of experience with programming, we highly recommend you get into the habit of writing unit tests for a few methods in your project. Any method that outputs data and doesn't talk to a database can be unit tested. + +## Features + - SQLite database connection + -- The program uses a SQLite DB connection to store and read information. + -- If no database exists, or the correct table does not exist, it will be created on program start. + - A console based UI where users can navigate by entering options. + ![Main menu](./ReadMeAssets/MainMenu.png) + - CRUD DB functions + -- From the main menu users can Create, Read, Update or Delete entries for whichever coding session they want. They need to enter the starting date and hour and the ending date and hour (format : dd-MMM-yyyy hh:MM). + -- User input are automatically checked to make sure they are in the correct and realistic format. + - Registered Habit output. + ![Coding Sessions Display](./ReadMeAssets/CodingSessionsDisplay.png) + +## How to run it + +### Prerequisites + +- [.NET SDK](https://dotnet.microsoft.com/download) 10.0 or later + +### Steps + +```bash +git clone https://github.com/Sephydev/STUDY.CodingTracker.git +cd HabbitLogger/STUDY.CodingTracker +dotnet run +``` + +## Challenges + - It was my first time using Dapper. I had to search for a good documentation to see the differences with ADO.NET. + - Managing `DateTime` and `TimeSpan` type of data was also challenging because of the differences with typical type like `int` or `string`. I mainly used Microsoft Learn documentation to solve the different problems I encountered. + +## Lesson Learned + - Coming from a JS background, I now see the differences with OOP programming. I can now see the benefits of OOP, notably how well the code is now organized and easy to read. + - I understand now the benefits of using Dapper. It allow me to "skip" some code that I used using ADO.NET, making my code more readable and making my work done quicker. + - Spectre.Console is a really good console package, making the UI more intuitive and beautiful, while being less tedious than using Console methods. It also give me basic validation. + +## Areas to improve + - I need to get better with git commit. I have a tendency of forgetting to commit when something is done. In this case, I commit when two or three things is done. It is not optimal. + - I have the feeling that my code could be more organized too. I will train that by practicing on new projects. + +## Resources Used + - C# Academy for the specs and related articles : https://www.thecsharpacademy.com/project/13/coding-tracker + - SQLitetutorial to learn the basic SQL command : https://www.sqlitetutorial.net/ + - Microsoft Learn on DateTime Struct official documentation to learn basic usage of DateTime: https://learn.microsoft.com/fr-fr/dotnet/api/system.datetime?view=net-8.0 + - Microsoft Learn on TimeSpan Struct official documentation to learn basic usage of TimeSpan: https://learn.microsoft.com/fr-fr/dotnet/api/system.timespan?view=net-8.0 + - This article helped me a lot seeing the differences between Dapper and ADO.NET, and to understand basic use of Dapper : https://medium.com/@pavanpitthdiya/the-ultimate-guide-to-dapper-in-net-everything-you-need-to-know-2025-edition-295ab8a4ced8 + - Spectre.Console documentation : https://spectreconsole.net/console \ No newline at end of file diff --git a/STUDY.CodingTracker.Sephydev/ReadMeAssets/CodingSessionsDisplay.png b/STUDY.CodingTracker.Sephydev/ReadMeAssets/CodingSessionsDisplay.png new file mode 100644 index 000000000..1a5065d45 Binary files /dev/null and b/STUDY.CodingTracker.Sephydev/ReadMeAssets/CodingSessionsDisplay.png differ diff --git a/STUDY.CodingTracker.Sephydev/ReadMeAssets/MainMenu.png b/STUDY.CodingTracker.Sephydev/ReadMeAssets/MainMenu.png new file mode 100644 index 000000000..926b84bd8 Binary files /dev/null and b/STUDY.CodingTracker.Sephydev/ReadMeAssets/MainMenu.png differ diff --git a/STUDY.CodingTracker.Sephydev/STUDY.CodingTracker.UnitTest/CodingTrackerTests.cs b/STUDY.CodingTracker.Sephydev/STUDY.CodingTracker.UnitTest/CodingTrackerTests.cs new file mode 100644 index 000000000..0b5268571 --- /dev/null +++ b/STUDY.CodingTracker.Sephydev/STUDY.CodingTracker.UnitTest/CodingTrackerTests.cs @@ -0,0 +1,128 @@ +using STUDY.CodingTracker.Helper; + +namespace STUDY.CodingTracker.UnitTest; + +public class CodingTrackerTests +{ + private static readonly object[] isValidDate = + { + new TestCaseData("05/05/2026 10:35", (true, Convert.ToDateTime("05/05/2026 10:35"))), + + new TestCaseData("05:05:2026 10:35", (false, new DateTime())), + new TestCaseData("zefzf", (false, new DateTime())), + new TestCaseData("zoief 10:35", (false, new DateTime())), + new TestCaseData("05/05/2026 fzef", (false, new DateTime())), + new TestCaseData("05/05/2026", (false, new DateTime())), + new TestCaseData("10:35", (false, new DateTime())), + new TestCaseData("32/05/2026 10:35", (false, new DateTime())), + new TestCaseData("00/05/2026 10:35", (false, new DateTime())), + new TestCaseData("05/13/2026 10:35", (false, new DateTime())), + new TestCaseData("05/00/2026 10:35", (false, new DateTime())), + new TestCaseData("05/05/2026 25:35", (false, new DateTime())), + new TestCaseData("05/05/2026 -01:35", (false, new DateTime())), + new TestCaseData("05/05/2026 10:66", (false, new DateTime())), + new TestCaseData("05/05/2026 10:-01", (false, new DateTime())) + }; + + private static readonly object[] isValidEndDate = + { + new TestCaseData("05/05/2026 10:35", Convert.ToDateTime("04/05/2026 10:35"), (true, Convert.ToDateTime("05/05/2026 10:35"))), + new TestCaseData("05/05/2026 10:35", Convert.ToDateTime("05/04/2026 10:35"), (true, Convert.ToDateTime("05/05/2026 10:35"))), + new TestCaseData("05/05/2026 10:35", Convert.ToDateTime("05/05/2025 10:35"), (true, Convert.ToDateTime("05/05/2026 10:35"))), + new TestCaseData("05/05/2026 10:35", Convert.ToDateTime("05/05/2026 09:35"), (true, Convert.ToDateTime("05/05/2026 10:35"))), + new TestCaseData("05/05/2026 10:35", Convert.ToDateTime("05/05/2026 10:30"), (true, Convert.ToDateTime("05/05/2026 10:35"))), + + new TestCaseData("05/05/2026 10:35", Convert.ToDateTime("05/05/2026 10:35"), (false, Convert.ToDateTime("05/05/2026 10:35"))), + new TestCaseData("05/05/2026 10:35", Convert.ToDateTime("06/05/2026 10:35"), (false, Convert.ToDateTime("05/05/2026 10:35"))), + new TestCaseData("05/05/2026 10:35", Convert.ToDateTime("05/06/2026 10:35"), (false, Convert.ToDateTime("05/05/2026 10:35"))), + new TestCaseData("05/05/2026 10:35", Convert.ToDateTime("05/05/2027 10:35"), (false, Convert.ToDateTime("05/05/2026 10:35"))), + new TestCaseData("05/05/2026 10:35", Convert.ToDateTime("05/05/2026 11:35"), (false, Convert.ToDateTime("05/05/2026 10:35"))), + new TestCaseData("05/05/2026 10:35", Convert.ToDateTime("05/05/2026 10:40"), (false, Convert.ToDateTime("05/05/2026 10:35"))), + }; + + private static readonly object[] isValidId = + { + new TestCaseData("5", (true, 5)), + + new TestCaseData("rerg", (false, 0)), + new TestCaseData("-1", (false, -1)), + new TestCaseData("0", (false, 0)) + }; + + private static readonly object[] isValidPeriod = + { + new TestCaseData("5", (true, 5)), + new TestCaseData("gerg", (false, 0)) + }; + + private static readonly object[] isValidWeek = + { + new TestCaseData("3", (true, 3)), + + new TestCaseData("ziufhz", (false, 0)), + new TestCaseData("-1", (false, -1)), + new TestCaseData("0", (false, 0)), + new TestCaseData("54", (false, 54)) + }; + + private static readonly object[] isValidDay = + { + new TestCaseData("3", (true, 3)), + + new TestCaseData("ziufhz", (false, 0)), + new TestCaseData("-1", (false, -1)), + new TestCaseData("0", (false, 0)), + new TestCaseData("32", (false, 32)) + }; + + private static readonly object[] isValidYear = + { + new TestCaseData("2020", (true, 2020)), + + new TestCaseData("ziufhz", (false, 0)), + new TestCaseData("1899", (false, 1899)), + new TestCaseData("2050", (false, 2050)), + }; + + [TestCaseSource(nameof(isValidDate))] + public void CorrectDate_ReturnsExpectedResult(string? input, (bool, DateTime) expectedValue) + { + (bool, DateTime) isValidDate = Verification.VerifyDate(input); + Assert.That(isValidDate, Is.EqualTo(expectedValue)); + } + + [TestCaseSource(nameof(isValidEndDate))] + public void CorrectEndDate_ReturnsExpectedResult(string? endDateInput, DateTime startDate, (bool, DateTime) expectedValue) + { + (bool, DateTime) isValidEndDate = Verification.VerifyEndDate(endDateInput, startDate); + Assert.That(isValidEndDate, Is.EqualTo(expectedValue)); + } + + [TestCaseSource(nameof(isValidId))] + public void CorrectId_ReturnsExpectedResult(string? input, (bool, int) expectedValue) + { + (bool, int) isValidId = Verification.VerifyId(input); + Assert.That(isValidId, Is.EqualTo(expectedValue)); + } + + [TestCaseSource(nameof(isValidWeek))] + public void CorrectWeek_ReturnsExpectedResult(string? input, (bool, int) expectedValue) + { + (bool, int) isValidWeek = Verification.VerifyWeek(input); + Assert.That(isValidWeek, Is.EqualTo(expectedValue)); + } + + [TestCaseSource(nameof(isValidDay))] + public void CorrectDay_ReturnsExpectedResult(string? input, (bool, int) expectedValue) + { + (bool, int) isValidDay = Verification.VerifyDay(input); + Assert.That(isValidDay, Is.EqualTo(expectedValue)); + } + + [TestCaseSource(nameof(isValidYear))] + public void CorrectYear_ReturnsExpectedResult(string? input, (bool, int) expectedValue) + { + (bool, int) isValidYear = Verification.VerifyYear(input); + Assert.That(isValidYear, Is.EqualTo(expectedValue)); + } +} diff --git a/STUDY.CodingTracker.Sephydev/STUDY.CodingTracker.UnitTest/STUDY.CodingTracker.UnitTest.csproj b/STUDY.CodingTracker.Sephydev/STUDY.CodingTracker.UnitTest/STUDY.CodingTracker.UnitTest.csproj new file mode 100644 index 000000000..4bf913357 --- /dev/null +++ b/STUDY.CodingTracker.Sephydev/STUDY.CodingTracker.UnitTest/STUDY.CodingTracker.UnitTest.csproj @@ -0,0 +1,27 @@ + + + + net10.0 + latest + enable + enable + false + + + + + + + + + + + + + + + + + + + diff --git a/STUDY.CodingTracker.Sephydev/STUDY.CodingTracker.slnx b/STUDY.CodingTracker.Sephydev/STUDY.CodingTracker.slnx new file mode 100644 index 000000000..0d57064d0 --- /dev/null +++ b/STUDY.CodingTracker.Sephydev/STUDY.CodingTracker.slnx @@ -0,0 +1,4 @@ + + + + diff --git a/STUDY.CodingTracker.Sephydev/STUDY.CodingTracker/Controllers/CodingSessionController.cs b/STUDY.CodingTracker.Sephydev/STUDY.CodingTracker/Controllers/CodingSessionController.cs new file mode 100644 index 000000000..21579da8a --- /dev/null +++ b/STUDY.CodingTracker.Sephydev/STUDY.CodingTracker/Controllers/CodingSessionController.cs @@ -0,0 +1,166 @@ +using Dapper; +using Microsoft.Data.Sqlite; +using Microsoft.Extensions.Configuration; +using STUDY.CodingTracker.Helper; +using STUDY.CodingTracker.Models; +using System.Globalization; + +namespace STUDY.CodingTracker.Controllers; + +internal class CodingSessionController +{ + private readonly IConfiguration _config; + private readonly string _connectionString; + + public CodingSessionController(IConfiguration config) + { + _config = config; + var section = _config.GetSection("DataBaseSettings"); + _connectionString = section["connectionString"] + section["databasePath"]; + + CreateTable(); + } + + public List GetCodingSessions(FilterChoice filterChoice, int periodNum, OrderChoice orderChoice) + { + List codingSessions = new(); + List filteredCodingSessions = new List(); + + string order = orderChoice == OrderChoice.Descending ? "ORDER BY STARTTIME DESC" : orderChoice == OrderChoice.Ascending ? "ORDER BY STARTTIME ASC" : ""; + + string command = $@" + SELECT STARTTIME start, + ENDTIME end, + ID newId, + DURATION newDuration FROM codingSessions + {order} + "; + + try + { + using var connection = new SqliteConnection(_connectionString); + + connection.Open(); + + codingSessions = connection.Query(command).AsList(); + } + catch (SqliteException e) + { + DBErrorMessage("getting the saved coding sessions", e.Message); + } + + filteredCodingSessions = FilterCodingSession(filterChoice, periodNum, codingSessions); + + return filteredCodingSessions; + } + + private static List FilterCodingSession(FilterChoice filterChoice, int periodNum, List codingSessions) + { + switch (filterChoice) + { + case FilterChoice.Week: + return codingSessions.FindAll(c => ISOWeek.GetWeekOfYear(c.startTime) == periodNum); + case FilterChoice.Day: + return codingSessions.FindAll(c => c.startTime.Day == periodNum); + case FilterChoice.Year: + return codingSessions.FindAll(c => c.startTime.Year == periodNum); + default: + return codingSessions; + } + } + + public bool AddCodingSession(CodingSessionModel codingSession) + { + bool success = false; + + try + { + using var connection = new SqliteConnection(_connectionString); + + connection.Open(); + + string sql = "INSERT INTO codingSessions (STARTTIME, ENDTIME, DURATION) VALUES (@StartTime, @EndTime, @Duration)"; + var parameters = new { @StartTime = codingSession.startTime, @EndTime = codingSession.endTime, @Duration = codingSession.duration }; + connection.Execute(sql, parameters); + + success = true; + } + catch (SqliteException e) + { + DBErrorMessage("adding the coding session", e.Message); + } + + return success; + } + + public int DeleteCodingSession(int idToDelete) + { + int numberOfRowsDeleted = 0; + + try + { + using var connection = new SqliteConnection(_connectionString); + + connection.Open(); + + numberOfRowsDeleted = connection.Execute("DELETE FROM codingSessions WHERE ID = @IdToDelete", new { @IdToDelete = idToDelete }); + } + catch (SqliteException e) + { + DBErrorMessage("deleting the coding session", e.Message); + } + + return numberOfRowsDeleted; + } + + public int UpdateCodingSession(int idToUpdate, CodingSessionModel updatedCodingSession) + { + int numberOfRowsUpdated = 0; + + try + { + using var connection = new SqliteConnection(_connectionString); + + connection.Open(); + + string sql = "UPDATE codingSessions SET STARTTIME = @StartTime, ENDTIME = @EndTime, DURATION = @Duration WHERE ID = @IdToUpdate"; + var parameters = new { @StartTime = updatedCodingSession.startTime, @EndTime = updatedCodingSession.endTime, @Duration = updatedCodingSession.duration, @IdToUpdate = idToUpdate }; + + numberOfRowsUpdated = connection.Execute(sql, parameters); + } + catch (SqliteException e) + { + DBErrorMessage("updating the coding session", e.Message); + } + + return numberOfRowsUpdated; + } + + private void CreateTable() + { + try + { + using var connection = new SqliteConnection(_connectionString); + + connection.Open(); + + connection.Execute( + @"CREATE TABLE IF NOT EXISTS codingSessions( + ID INTEGER PRIMARY KEY AUTOINCREMENT, + STARTTIME TEXT, + ENDTIME TEXT, + DURATION TEXT + )" + ); + } + catch (SqliteException e) + { + DBErrorMessage("creating the DB Table", e.Message); + } + } + + private void DBErrorMessage(string action, string errorMessage) + { + Console.WriteLine($"An error occured while {action}. Error: {errorMessage}"); + } +} diff --git a/STUDY.CodingTracker.Sephydev/STUDY.CodingTracker/Helper/Enums.cs b/STUDY.CodingTracker.Sephydev/STUDY.CodingTracker/Helper/Enums.cs new file mode 100644 index 000000000..84adccb9f --- /dev/null +++ b/STUDY.CodingTracker.Sephydev/STUDY.CodingTracker/Helper/Enums.cs @@ -0,0 +1,31 @@ +namespace STUDY.CodingTracker.Helper; + +public enum MainMenuChoice +{ + ViewCodingSessions, + AddCodingSession, + DeleteCodingSession, + UpdateCodingSession, + Exit +} + +public enum FilterChoice +{ + Week, + Day, + Year, + None +} + +public enum StopwatchChoice +{ + Yes, + No +} + +public enum OrderChoice +{ + Descending, + Ascending, + None +} \ No newline at end of file diff --git a/STUDY.CodingTracker.Sephydev/STUDY.CodingTracker/Helper/StopWatch.cs b/STUDY.CodingTracker.Sephydev/STUDY.CodingTracker/Helper/StopWatch.cs new file mode 100644 index 000000000..52b7cce29 --- /dev/null +++ b/STUDY.CodingTracker.Sephydev/STUDY.CodingTracker/Helper/StopWatch.cs @@ -0,0 +1,43 @@ +using Spectre.Console; +using System.Timers; + +namespace STUDY.CodingTracker.Helper; + +internal class StopWatch +{ + private static System.Timers.Timer _timer; + private TimeSpan duration = new TimeSpan(0, 0, 0); + + public StopWatch() + { + duration = new TimeSpan(0, 0, 0); + } + + public TimeSpan LaunchTimer() + { + SetTimer(); + + Console.ReadLine(); + + _timer.Stop(); + _timer.Dispose(); + + return duration; + } + + private void SetTimer() + { + _timer = new System.Timers.Timer(1000); + _timer.Elapsed += OnTimedEvent; + _timer.AutoReset = true; + _timer.Enabled = true; + } + + private void OnTimedEvent(Object source, ElapsedEventArgs e) + { + duration = duration.Add(new TimeSpan(0, 0, 1)); + Console.Clear(); + AnsiConsole.MarkupLine("Press Any Key to stop the timer."); + AnsiConsole.MarkupLine($"Duration: {duration}"); + } +} diff --git a/STUDY.CodingTracker.Sephydev/STUDY.CodingTracker/Helper/UserInput.cs b/STUDY.CodingTracker.Sephydev/STUDY.CodingTracker/Helper/UserInput.cs new file mode 100644 index 000000000..e3022a27b --- /dev/null +++ b/STUDY.CodingTracker.Sephydev/STUDY.CodingTracker/Helper/UserInput.cs @@ -0,0 +1,28 @@ +using Spectre.Console; + +namespace STUDY.CodingTracker.Helper; + +internal static class UserInput +{ + public static string GetUserDateInput(string period) + { + string dateInput = AnsiConsole.Ask($"Please enter the {period} date (format: dd/MM/yyyy HH:mm):"); + return dateInput; + } + + public static string GetUserIDInput(string operation) + { + string idInput = AnsiConsole.Ask($"Please enter the id of the coding session you want to {operation}:"); + + return idInput; + } + + public static string GetUserFilterPeriod(FilterChoice filterChoice) + { + Console.Clear(); + + string periodInput = AnsiConsole.Ask($"Please enter the {filterChoice} number you want the coding sessions displayed be filtered on:"); + + return periodInput; + } +} diff --git a/STUDY.CodingTracker.Sephydev/STUDY.CodingTracker/Helper/Verification.cs b/STUDY.CodingTracker.Sephydev/STUDY.CodingTracker/Helper/Verification.cs new file mode 100644 index 000000000..945db6212 --- /dev/null +++ b/STUDY.CodingTracker.Sephydev/STUDY.CodingTracker/Helper/Verification.cs @@ -0,0 +1,90 @@ +using System.Globalization; + +namespace STUDY.CodingTracker.Helper; + +public static class Verification +{ + public static (bool correct, DateTime date) VerifyDate(string dateInput) + { + bool correct = false; + DateTime date = new DateTime(); + + if (dateInput != null && DateTime.TryParseExact(dateInput, "dd/MM/yyyy HH:mm", new CultureInfo("en-US"), DateTimeStyles.None, out date)) + { + correct = true; + } + + return (correct, date); + } + + public static (bool correct, DateTime date) VerifyEndDate(string endDateInput, DateTime startDate) + { + bool correct = false; + var formatVerificationResult = VerifyDate(endDateInput); + + if (formatVerificationResult.correct && DateTime.Compare(formatVerificationResult.date, startDate) > 0) + { + correct = true; + } + + return (correct, formatVerificationResult.date); + } + + public static (bool correct, int id) VerifyId(string idInput) + { + bool correct = false; + int id = 0; + + if (idInput != null && int.TryParse(idInput, out id) && id > 0) + { + correct = true; + } + + return (correct, id); + } + + private static (bool, int) VerifyPeriod(string periodInput) + { + bool correct = false; + int periodNum = 0; + + if (periodInput != null && int.TryParse(periodInput, out periodNum)) + { + correct = true; + } + return (correct, periodNum); + } + + public static (bool correct, int periodNum) VerifyWeek(string weekInput) + { + bool correct = false; + (bool correct, int weekNum) basicVerificationResult = VerifyPeriod(weekInput); + + if (basicVerificationResult.correct && basicVerificationResult.weekNum > 0 && basicVerificationResult.weekNum < 54) + correct = true; + + return (correct, basicVerificationResult.weekNum); + } + + public static (bool correct, int periodNum) VerifyDay(string dayInput) + { + bool correct = false; + (bool correct, int dayNum) basicVerificationResult = VerifyPeriod(dayInput); + + if (basicVerificationResult.correct && basicVerificationResult.dayNum > 0 && basicVerificationResult.dayNum < 32) + correct = true; + + return (correct, basicVerificationResult.dayNum); + } + + public static (bool correct, int periodNum) VerifyYear(string yearInput) + { + bool correct = false; + (bool correct, int yearNum) basicVerificationResult = VerifyPeriod(yearInput); + + if (basicVerificationResult.correct && basicVerificationResult.yearNum >= 1900 && basicVerificationResult.yearNum <= DateTime.Now.Year) + correct = true; + + return (correct, basicVerificationResult.yearNum); + } +} diff --git a/STUDY.CodingTracker.Sephydev/STUDY.CodingTracker/Models/CodingSessionModel.cs b/STUDY.CodingTracker.Sephydev/STUDY.CodingTracker/Models/CodingSessionModel.cs new file mode 100644 index 000000000..978e5b76e --- /dev/null +++ b/STUDY.CodingTracker.Sephydev/STUDY.CodingTracker/Models/CodingSessionModel.cs @@ -0,0 +1,52 @@ +namespace STUDY.CodingTracker.Models; + +internal class CodingSessionModel +{ + public Int64 id { get; set; } + public DateTime startTime { get; set; } + public DateTime endTime { get; set; } + public TimeSpan duration { get; } + + public CodingSessionModel (string start, string end, Int64 newId, string newDuration) + { + id = newId; + + startTime = ConvertStringToDateTime(start); + + endTime = ConvertStringToDateTime(end); + + duration = ConvertStringToTimeSpan(newDuration); + } + + public CodingSessionModel (DateTime start, DateTime end, TimeSpan newDuration) + { + startTime = start; + endTime = end; + duration = newDuration; + } + + public CodingSessionModel (DateTime start, DateTime end) + { + startTime = start; + endTime = end; + duration = endTime.Subtract(startTime); + } + + private DateTime ConvertStringToDateTime(string date) + { + DateTime convertedDate; + + DateTime.TryParse(date, out convertedDate); + return convertedDate; + } + + private TimeSpan ConvertStringToTimeSpan(string time) + { + TimeSpan convertedTime; + + TimeSpan.TryParse(time, out convertedTime); + + return convertedTime; + } + +} diff --git a/STUDY.CodingTracker.Sephydev/STUDY.CodingTracker/Program.cs b/STUDY.CodingTracker.Sephydev/STUDY.CodingTracker/Program.cs new file mode 100644 index 000000000..450022d08 --- /dev/null +++ b/STUDY.CodingTracker.Sephydev/STUDY.CodingTracker/Program.cs @@ -0,0 +1,5 @@ +using STUDY.CodingTracker; + +UserInterface ui = new UserInterface(); + +ui.MainMenu(); diff --git a/STUDY.CodingTracker.Sephydev/STUDY.CodingTracker/Properties/launchSettings.json b/STUDY.CodingTracker.Sephydev/STUDY.CodingTracker/Properties/launchSettings.json new file mode 100644 index 000000000..1dcdfa2e7 --- /dev/null +++ b/STUDY.CodingTracker.Sephydev/STUDY.CodingTracker/Properties/launchSettings.json @@ -0,0 +1,8 @@ +{ + "profiles": { + "STUDY.CodingTracker": { + "commandName": "Project", + "workingDirectory": "C:\\C# Academy\\STUDY.CodingTracker\\STUDY.CodingTracker\\" + } + } +} \ No newline at end of file diff --git a/STUDY.CodingTracker.Sephydev/STUDY.CodingTracker/STUDY.CodingTracker.csproj b/STUDY.CodingTracker.Sephydev/STUDY.CodingTracker/STUDY.CodingTracker.csproj new file mode 100644 index 000000000..58e0862a6 --- /dev/null +++ b/STUDY.CodingTracker.Sephydev/STUDY.CodingTracker/STUDY.CodingTracker.csproj @@ -0,0 +1,18 @@ + + + + Exe + net10.0 + enable + enable + + + + + + + + + + + diff --git a/STUDY.CodingTracker.Sephydev/STUDY.CodingTracker/UserInterface.cs b/STUDY.CodingTracker.Sephydev/STUDY.CodingTracker/UserInterface.cs new file mode 100644 index 000000000..1754bfbeb --- /dev/null +++ b/STUDY.CodingTracker.Sephydev/STUDY.CodingTracker/UserInterface.cs @@ -0,0 +1,337 @@ +using Microsoft.Extensions.Configuration; +using Spectre.Console; +using STUDY.CodingTracker.Controllers; +using STUDY.CodingTracker.Helper; +using STUDY.CodingTracker.Models; +using System.Globalization; + +namespace STUDY.CodingTracker; + +internal class UserInterface +{ + private CodingSessionController _codingSessionController; + + public UserInterface() + { + // Can't move it to CodingSessionController because of "Directory.GetCurrentDirectory()" + IConfiguration config = new ConfigurationBuilder() + .SetBasePath(Directory.GetCurrentDirectory()) + .AddJsonFile("appsettings.json", optional: false, + reloadOnChange: true) + .Build(); + + _codingSessionController = new CodingSessionController(config); + } + + public void MainMenu() + { + while (true) + { + MainMenuChoice choice = GetMainMenuChoice(); + + switch (choice) + { + case MainMenuChoice.ViewCodingSessions: + FilterChoice filterChoice = AskFilter(); + int periodNum = AskPeriodNum(filterChoice); + OrderChoice orderChoice = AskOrder(); + + DisplayCodingSessionsTable(filterChoice, periodNum, orderChoice); + DisplayPressKeyToContinue(); + break; + case MainMenuChoice.AddCodingSession: + AskUserStopwatchChoice(); + DisplayPressKeyToContinue(); + break; + case MainMenuChoice.DeleteCodingSession: + DisplayDeleteCodingSessionUI(); + DisplayPressKeyToContinue(); + break; + case MainMenuChoice.UpdateCodingSession: + DisplayUpdateCodingSessionUI(); + DisplayPressKeyToContinue(); + break; + case MainMenuChoice.Exit: + AnsiConsole.MarkupLine("Thank you for using the app! See you soon!"); + return; + } + } + } + + private MainMenuChoice GetMainMenuChoice() + { + Console.Clear(); + + AnsiConsole.MarkupLine("Welcome to [cyan]Coding Tracker[/]!"); + AnsiConsole.MarkupLine("-----------------------------------"); + + MainMenuChoice choice = AnsiConsole.Prompt( + new SelectionPrompt() + .Title("Please select one of the option:") + .AddChoices(Enum.GetValues()) + ); + + return choice; + } + + private FilterChoice AskFilter() + { + Console.Clear(); + + FilterChoice filterChoice = AnsiConsole.Prompt( + new SelectionPrompt() + .Title("Please select one of the filter:") + .AddChoices(Enum.GetValues()) + ); + + return filterChoice; + } + + private int AskPeriodNum(FilterChoice filterChoice) + { + while (true) + { + string userInput = ""; + int periodNum = 0; + (bool correct, int periodNum) validationResult = (false, 0); + + if (filterChoice != FilterChoice.None) + { + userInput = UserInput.GetUserFilterPeriod(filterChoice); + } + else + { + return periodNum; + } + + switch (filterChoice) + { + case FilterChoice.Week: + validationResult = Verification.VerifyWeek(userInput); + break; + case FilterChoice.Day: + validationResult = Verification.VerifyDay(userInput); + break; + case FilterChoice.Year: + validationResult = Verification.VerifyYear(userInput); + break; + } + + if (validationResult.correct) + { + return validationResult.periodNum; + } + else + { + AnsiConsole.MarkupLine("[red]Please enter a correct number based on the period you've chosen.[/]"); + DisplayPressKeyToContinue(); + } + } + } + + private OrderChoice AskOrder() + { + OrderChoice orderChoice = AnsiConsole.Prompt( + new SelectionPrompt() + .Title("Please select one of the order:") + .AddChoices(Enum.GetValues()) + ); + + return orderChoice; + } + + private void DisplayCodingSessionsTable(FilterChoice filterChoice, int periodNum, OrderChoice orderChoice) + { + Console.Clear(); + + var codingSessions = _codingSessionController.GetCodingSessions(filterChoice, periodNum, orderChoice); + + var table = new Table().RoundedBorder().BorderColor(Color.Gold1); + + table.AddColumn("[DarkOrange]ID[/]"); + table.AddColumn("[DarkOrange]Start Time[/]"); + table.AddColumn("[DarkOrange]End Time[/]"); + table.AddColumn("[DarkOrange]Duration[/]"); + + foreach (CodingSessionModel codingSession in codingSessions) + { + table.AddRow( + $"[yellow]{codingSession.id.ToString()}[/]", + codingSession.startTime.ToString("dd/MMM/yyyy HH:mm:ss"), + codingSession.endTime.ToString("dd/MMM/yyyy HH:mm:ss"), + codingSession.duration.ToString() + ); + } + + AnsiConsole.Write(table); + } + + private void AskUserStopwatchChoice() + { + StopwatchChoice choice = AnsiConsole.Prompt( + new SelectionPrompt() + .Title("Do you wish to use the stopwatch?") + .AddChoices(Enum.GetValues()) + ); + + if (choice == StopwatchChoice.Yes) + ManageStopWatch(); + else + DisplayManualAddingCodingSessionUI(); + } + + private void ManageStopWatch() + { + bool success; + StopWatch stopwatch = new StopWatch(); + + DateTime startTime = DateTime.Now; + + TimeSpan duration = stopwatch.LaunchTimer(); + + DateTime endTime = DateTime.Now; + + CodingSessionModel newCodingSession = new CodingSessionModel(startTime, endTime, duration); + + success = _codingSessionController.AddCodingSession(newCodingSession); + + DisplaySuccessResultWhenAddingSession(success); + } + + private void DisplayManualAddingCodingSessionUI() + { + bool success; + + CodingSessionModel newCodingSession = CreateCodingSession(); + + success = _codingSessionController.AddCodingSession(newCodingSession); + + DisplaySuccessResultWhenAddingSession(success); + } + + private void DisplaySuccessResultWhenAddingSession(bool success) + { + if (success) + { + AnsiConsole.MarkupLine("[green]Coding session added successfully![/]"); + return; + } + + AnsiConsole.MarkupLine("[red]Coding session was not added...[/]"); + } + + private void DisplayDeleteCodingSessionUI() + { + FilterChoice filterChoice = AskFilter(); + int periodNum = AskPeriodNum(filterChoice); + OrderChoice orderChoice = AskOrder(); + + while (true) + { + DisplayCodingSessionsTable(filterChoice, periodNum, orderChoice); + + string idToDelete = UserInput.GetUserIDInput("delete"); + var verificationResult = Verification.VerifyId(idToDelete); + + if (!verificationResult.correct) + { + DisplayInputIDErrorMessage(); + continue; + } + int numberOfRows = _codingSessionController.DeleteCodingSession(verificationResult.id); + + if (numberOfRows > 0) + { + AnsiConsole.MarkupLine("[green]Coding session deleted successfully![/]"); + return; + } + + AnsiConsole.MarkupLine("[red]The id you entered doesn't exist.[/]"); + DisplayPressKeyToContinue(); + } + } + + private void DisplayUpdateCodingSessionUI() + { + FilterChoice filterChoice = AskFilter(); + int periodNum = AskPeriodNum(filterChoice); + OrderChoice orderChoice = AskOrder(); + + while (true) + { + DisplayCodingSessionsTable(filterChoice, periodNum, orderChoice); + + string idToUpdate = UserInput.GetUserIDInput("update"); + var verificationResult = Verification.VerifyId(idToUpdate); + + if (!verificationResult.correct) + { + DisplayInputIDErrorMessage(); + continue; + } + + CodingSessionModel updatedCodingSession = CreateCodingSession(); + + int numberOfRows = _codingSessionController.UpdateCodingSession(verificationResult.id, updatedCodingSession); + + if (numberOfRows > 0) + { + AnsiConsole.MarkupLine("[green]Coding session updated successfully![/]"); + return; + } + + AnsiConsole.MarkupLine("[red]The id you entered doesn't exist.[/]"); + } + } + + private void DisplayPressKeyToContinue() + { + AnsiConsole.MarkupLine("(Press Any Key to Continue.)"); + Console.ReadKey(); + } + + private CodingSessionModel CreateCodingSession() + { + while (true) + { + Console.Clear(); + + string startTime = UserInput.GetUserDateInput("start"); + var verificationResultStartTime = Verification.VerifyDate(startTime); + + if (!verificationResultStartTime.correct) + { + DisplayInputDateErrorMessage(); + continue; + } + + string endTime = UserInput.GetUserDateInput("end"); + var verificationResultEndTime = Verification.VerifyEndDate(endTime, verificationResultStartTime.date); + + if (!verificationResultEndTime.correct) + { + DisplayInputDateErrorMessage(); + continue; + } + + CodingSessionModel newCodingSession = new CodingSessionModel(verificationResultStartTime.date, verificationResultEndTime.date); + + return newCodingSession; + } + } + + private void DisplayInputIDErrorMessage() + { + AnsiConsole.MarkupLine("[red]You've inputted the ID in a wrong format. Please try again![/]"); + AnsiConsole.MarkupLine("Good format: [green]It must be a whole positive number.[/]"); + DisplayPressKeyToContinue(); + } + + private void DisplayInputDateErrorMessage() + { + AnsiConsole.MarkupLine("[red]You've inputted the date in a wrong format and/or in the wrong order. Please try again![/]"); + AnsiConsole.MarkupLine("Good format: [green]dd/MM/yyyy HH:mm:ss[/] (d = day, M = month, y = year, H = hour, m = minute, s = second)"); + AnsiConsole.MarkupLine("The start time must be [green]earlier[/] than end time."); + DisplayPressKeyToContinue(); + } +} diff --git a/STUDY.CodingTracker.Sephydev/STUDY.CodingTracker/appsettings.json b/STUDY.CodingTracker.Sephydev/STUDY.CodingTracker/appsettings.json new file mode 100644 index 000000000..521d664ec --- /dev/null +++ b/STUDY.CodingTracker.Sephydev/STUDY.CodingTracker/appsettings.json @@ -0,0 +1,6 @@ +{ + "DatabaseSettings": { + "connectionString": "Data Source=", + "databasePath": "./coding-tracker.db" + } +} \ No newline at end of file diff --git a/STUDY.CodingTracker.Sephydev/STUDY.CodingTracker/coding-tracker.db b/STUDY.CodingTracker.Sephydev/STUDY.CodingTracker/coding-tracker.db new file mode 100644 index 000000000..88a68f801 Binary files /dev/null and b/STUDY.CodingTracker.Sephydev/STUDY.CodingTracker/coding-tracker.db differ