diff --git a/santisica29.CodingTracker/.gitattributes b/santisica29.CodingTracker/.gitattributes new file mode 100644 index 00000000..1ff0c423 --- /dev/null +++ b/santisica29.CodingTracker/.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/santisica29.CodingTracker/.gitignore b/santisica29.CodingTracker/.gitignore new file mode 100644 index 00000000..9491a2fd --- /dev/null +++ b/santisica29.CodingTracker/.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/santisica29.CodingTracker/CodingTracker.sln b/santisica29.CodingTracker/CodingTracker.sln new file mode 100644 index 00000000..ea83d12d --- /dev/null +++ b/santisica29.CodingTracker/CodingTracker.sln @@ -0,0 +1,30 @@ + +Microsoft Visual Studio Solution File, Format Version 12.00 +# Visual Studio Version 17 +VisualStudioVersion = 17.13.35931.197 +MinimumVisualStudioVersion = 10.0.40219.1 +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "CodingTracker", "CodingTracker\CodingTracker.csproj", "{B21877BE-CAFE-4F78-B77E-FA87AC3123D2}" +EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution Items", "{8EC462FD-D22E-90A8-E5CE-7E832BA40C5D}" + ProjectSection(SolutionItems) = preProject + README.md = README.md + EndProjectSection +EndProject +Global + GlobalSection(SolutionConfigurationPlatforms) = preSolution + Debug|Any CPU = Debug|Any CPU + Release|Any CPU = Release|Any CPU + EndGlobalSection + GlobalSection(ProjectConfigurationPlatforms) = postSolution + {B21877BE-CAFE-4F78-B77E-FA87AC3123D2}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {B21877BE-CAFE-4F78-B77E-FA87AC3123D2}.Debug|Any CPU.Build.0 = Debug|Any CPU + {B21877BE-CAFE-4F78-B77E-FA87AC3123D2}.Release|Any CPU.ActiveCfg = Release|Any CPU + {B21877BE-CAFE-4F78-B77E-FA87AC3123D2}.Release|Any CPU.Build.0 = Release|Any CPU + EndGlobalSection + GlobalSection(SolutionProperties) = preSolution + HideSolutionNode = FALSE + EndGlobalSection + GlobalSection(ExtensibilityGlobals) = postSolution + SolutionGuid = {078DE824-21F4-4A7C-84E4-2C66C050E0D2} + EndGlobalSection +EndGlobal diff --git a/santisica29.CodingTracker/CodingTracker/App.config b/santisica29.CodingTracker/CodingTracker/App.config new file mode 100644 index 00000000..1a6d3b55 --- /dev/null +++ b/santisica29.CodingTracker/CodingTracker/App.config @@ -0,0 +1,6 @@ + + + + + + diff --git a/santisica29.CodingTracker/CodingTracker/CodingTracker.csproj b/santisica29.CodingTracker/CodingTracker/CodingTracker.csproj new file mode 100644 index 00000000..0774358b --- /dev/null +++ b/santisica29.CodingTracker/CodingTracker/CodingTracker.csproj @@ -0,0 +1,28 @@ + + + + Exe + net8.0 + enable + enable + + + + + tlbimp + 4 + 2 + bee4bfec-6683-3e67-9167-3c0cbc68f40a + + + + + + + + + + + + + diff --git a/santisica29.CodingTracker/CodingTracker/Controller/CodingController.cs b/santisica29.CodingTracker/CodingTracker/Controller/CodingController.cs new file mode 100644 index 00000000..951eb74e --- /dev/null +++ b/santisica29.CodingTracker/CodingTracker/Controller/CodingController.cs @@ -0,0 +1,214 @@ +using CodingTracker.Data; +using CodingTracker.Models; +using Dapper; +using Microsoft.Data.Sqlite; +using Spectre.Console; +using static CodingTracker.Enums; +using System.Globalization; + +namespace CodingTracker.Controller; +internal class CodingController +{ + private readonly DatabaseMethods databaseMethods = new(); + + public void AddSession(string? startTime = null, string? endTime = null) + { + if (startTime == null || endTime == null) + { + startTime = Helpers.GetDateInput("Enter the start time of your coding session (yyyy-MM-dd HH:mm).\nPress 't' to enter actual time."); + + endTime = Helpers.GetDateInput("Enter the end time of your coding session (yyyy-MM-dd HH:mm)\nPress 't' to enter actual time."); + } + + while (Helpers.IsEndTimeLowerThanStartTime(startTime, endTime)) + { + endTime = Helpers.GetDateInput("Invalid input. End time must be larger than the start time."); + } + + var session = new CodingSession( + DateTime.ParseExact(startTime, "yyyy-MM-dd HH:mm", new CultureInfo("en-US")), + DateTime.ParseExact(endTime, "yyyy-MM-dd HH:mm", new CultureInfo("en-US")) + ); + + var newSession = new { StartTime = startTime, EndTime = endTime, Duration = session.CalculateDuration().ToString() }; + + var affectedRows = databaseMethods.CreateSession(newSession); + + if (affectedRows > 0) Helpers.DisplayMessage("Addition completed.", "green"); + else Helpers.DisplayMessage("No changes made"); + + AnsiConsole.MarkupLine("Press any key to continue."); + Console.ReadKey(); + } + + public void DeleteSession() + { + var list = GetSessions(); + + Helpers.CheckIfListIsNullOrEmpty(list); + + var sessionToDelete = AnsiConsole.Prompt( + new SelectionPrompt() + .Title("Select a [red]session[/] to delete:") + .UseConverter(s => $"{s.Id} - {s.StartTime} - {s.EndTime} - {s.Duration}") + .AddChoices(list)); + + if (Helpers.ConfirmDeletion(sessionToDelete.ToString())) + { + var affectedRows = databaseMethods.DeleteSession(sessionToDelete); + + if (affectedRows > 0) Helpers.DisplayMessage("Deletion completed.", "green"); + else Helpers.DisplayMessage("No changes made"); + } + else + { + Helpers.DisplayMessage("Deletion canceled", "yellow"); + } + + AnsiConsole.MarkupLine("Press any key to continue."); + Console.ReadKey(); + } + + public void UpdateSession() + { + var list = GetSessions(); + + Helpers.CheckIfListIsNullOrEmpty(list); + + var sessionToUpdate = AnsiConsole.Prompt( + new SelectionPrompt() + .Title("Select a [red]session[/] to update:") + .UseConverter(s => $"{s.Id} - {s.StartTime} - {s.EndTime} - {s.Duration}") + .AddChoices(list)); + + var newStartTime = Helpers.GetDateInput("Enter the start time of your coding session (yyyy-MM-dd HH:mm)"); + var newEndTime = Helpers.GetDateInput("Enter the end time of your coding session (yyyy-MM-dd HH:mm)"); + + while (Helpers.IsEndTimeLowerThanStartTime(newStartTime, newEndTime)) + { + newEndTime = Helpers.GetDateInput("Invalid input. End time must be higher than start time."); + } + + var session = new CodingSession( + DateTime.ParseExact(newStartTime, "yyyy-MM-dd HH:mm", new CultureInfo("en-US")), + DateTime.ParseExact(newEndTime, "yyyy-MM-dd HH:mm", new CultureInfo("en-US")) + ); + + var newSession = new { Id = sessionToUpdate.Id, NewStartTime = newStartTime, NewEndTime = newEndTime, Duration = session.CalculateDuration().ToString()}; + + var affectedRows = databaseMethods.UpdateSession(newSession); + + if (affectedRows > 0) Helpers.DisplayMessage("Update successful.", "green"); + else Helpers.DisplayMessage("No changes made"); + + AnsiConsole.MarkupLine("Press any key to continue."); + Console.ReadKey(); + } + public void StartSession() + { + Helpers.DisplayMessage("Session started", "blue"); + var startTime = DateTime.Now.ToString("yyyy-MM-dd HH:mm"); + + var timer = Helpers.RunStopWatch(); + + var choice = AnsiConsole.Confirm("Do you want to save your session?"); + + if (!choice) return; + + var endTime = timer; + + AddSession(startTime, endTime); + } + + public List? GetSessions(string? sql = null) + { + using var connection = new SqliteConnection(DatabaseInitializer.GetConnectionString()); + + if (sql == null) sql = $"SELECT * FROM coding_tracker"; + + var listFromDB = connection.Query(sql).ToList(); + + if (listFromDB.Count == 0) return null; + + var listOfCodingSessions = Helpers.ParseAnonObjToCodingSession(listFromDB); + + return listOfCodingSessions; + } + + public void ViewSessions(List? list = null, List additionalList = null) + { + Helpers.CheckIfListIsNullOrEmpty(list); + + Helpers.CreateTable(list, ["ID", "Start Time", "End Time", "Duration"]); + + if (additionalList != null) + { + Helpers.CreateTableOfAvg(additionalList); + } + + AnsiConsole.MarkupLine("Press Any Key to Continue."); + Console.ReadKey(); + } + + public void ViewReportOfCodingSession() + { + Helpers.DisplayMessage("Get your coding tracker report!"); + + var choice = AnsiConsole.Prompt( + new SelectionPrompt() + .Title("Select your report type") + .AddChoices(Enum.GetValues()) + ); + + string? unit = null; + + if (choice != ReportOption.Total) + { + unit = AnsiConsole.Prompt( + new TextPrompt($"Select the number of {choice} for your report.")); + } + + var listOfReport = GetReport(choice, unit); + + var additionalList = GetReportOfTotalAndAvg(choice, unit); + + ViewSessions(listOfReport, additionalList); + } + + public List? GetReport(ReportOption choice, string? unit) + { + var sql = $"SELECT * FROM coding_tracker "; + + _ = choice switch + { + ReportOption.Days => sql += $"WHERE EndTime > date('now', '-{unit} days')", + ReportOption.Months => sql += $"WHERE EndTime > date('now','start of month', '-{unit} months')", + ReportOption.Years => sql += $"WHERE EndTime > date('now','start of year', '-{unit} years')", + ReportOption.Total => sql + }; + + sql += " ORDER BY StartTime DESC"; + + return GetSessions(sql); + } + + public List? GetReportOfTotalAndAvg(ReportOption choice, string? unit) + { + using var connection = new SqliteConnection(DatabaseInitializer.GetConnectionString()); + + var sql = $"SELECT Duration FROM coding_tracker "; + _ = choice switch + { + ReportOption.Days => sql += $"WHERE EndTime > date('now', '-{unit} days')", + ReportOption.Months => sql += $"WHERE EndTime > date('now','start of month', '-{unit} months')", + ReportOption.Years => sql += $"WHERE EndTime > date('now','start of year', '-{unit} years')", + ReportOption.Total => sql + }; + + sql += " ORDER BY StartTime DESC"; + + var list = connection.Query(sql).ToList(); + + return list; + } +} diff --git a/santisica29.CodingTracker/CodingTracker/Data/DatabaseInitializer.cs b/santisica29.CodingTracker/CodingTracker/Data/DatabaseInitializer.cs new file mode 100644 index 00000000..b2c4c032 --- /dev/null +++ b/santisica29.CodingTracker/CodingTracker/Data/DatabaseInitializer.cs @@ -0,0 +1,39 @@ +using Microsoft.Data.Sqlite; +using System.Configuration; + +namespace CodingTracker.Data; +internal static class DatabaseInitializer +{ + internal static string GetConnectionString() + { + string dbPath = Path.Combine(ProjectRoot(), GetDBName()); + string connectionString = $"Data Source={dbPath}.db"; + return connectionString; + } + + internal static string GetDBName() + { + return ConfigurationManager.AppSettings["DatabaseFileName"]; + } + + internal static string ProjectRoot() + { + return Path.GetFullPath(Path.Combine(AppContext.BaseDirectory, @"..\..\..")); + } + internal static void CreateDatabase() + { + using var connection = new SqliteConnection(GetConnectionString()); + connection.Open(); + var tableCmd = connection.CreateCommand(); + + tableCmd.CommandText = + @$"CREATE TABLE IF NOT EXISTS {GetDBName()}( + Id INTEGER PRIMARY KEY AUTOINCREMENT, + StartTime TEXT, + EndTime TEXT, + Duration TEXT + )"; + + tableCmd.ExecuteNonQuery(); + } +} diff --git a/santisica29.CodingTracker/CodingTracker/Data/DatabaseMethods.cs b/santisica29.CodingTracker/CodingTracker/Data/DatabaseMethods.cs new file mode 100644 index 00000000..4e302dd9 --- /dev/null +++ b/santisica29.CodingTracker/CodingTracker/Data/DatabaseMethods.cs @@ -0,0 +1,47 @@ +using CodingTracker.Models; +using Dapper; +using Microsoft.Data.Sqlite; +using System.Globalization; + +namespace CodingTracker.Data; +internal class DatabaseMethods +{ + public int CreateSession(object newSession) + { + using var connection = new SqliteConnection(DatabaseInitializer.GetConnectionString()); + + var sql = + @$"INSERT INTO {DatabaseInitializer.GetDBName()} (startTime, endTime, duration) + VALUES (@StartTime, @EndTime, @Duration)"; + + var affectedRows = connection.Execute(sql, newSession); + + return affectedRows; + } + + public int DeleteSession(CodingSession sessionToDelete) + { + using var connection = new SqliteConnection(DatabaseInitializer.GetConnectionString()); + var sql = $@"DELETE from {DatabaseInitializer.GetDBName()} WHERE Id = @Id"; + + var affectedRows = connection.Execute(sql, new { sessionToDelete.Id }); + + return affectedRows; + } + + public int UpdateSession(object session) + { + using var connection = new SqliteConnection(DatabaseInitializer.GetConnectionString()); + + var sql = @$"UPDATE {DatabaseInitializer.GetDBName()} + SET StartTime = @NewStartTime, + EndTime = @NewEndTime, + Duration = @Duration + WHERE Id = @Id"; + + var affectedRows = connection.Execute(sql, session); + + return affectedRows; + + } +} diff --git a/santisica29.CodingTracker/CodingTracker/Enums.cs b/santisica29.CodingTracker/CodingTracker/Enums.cs new file mode 100644 index 00000000..6fd226bf --- /dev/null +++ b/santisica29.CodingTracker/CodingTracker/Enums.cs @@ -0,0 +1,32 @@ +using System.ComponentModel; +using System.ComponentModel.DataAnnotations; + +namespace CodingTracker; +internal class Enums +{ + internal enum MenuOption + { + [Description("Add Coding Session")] + AddCodingSession, + [Description("View Coding Session")] + ViewCodingSession, + [Description("Delete Coding Session")] + DeleteCodingSession, + [Description("Update Coding Session")] + UpdateCodingSession, + [Description("Start Session")] + StartSession, + [Description("View Report of Coding Session")] + ViewReportOfCodingSession, + [Description("Exit")] + Exit + } + + internal enum ReportOption + { + Days, + Months, + Years, + Total + } +} diff --git a/santisica29.CodingTracker/CodingTracker/Helpers.cs b/santisica29.CodingTracker/CodingTracker/Helpers.cs new file mode 100644 index 00000000..69e9ea75 --- /dev/null +++ b/santisica29.CodingTracker/CodingTracker/Helpers.cs @@ -0,0 +1,216 @@ +using CodingTracker.Models; +using CodingTracker.View; +using ConsoleTableExt; +using Spectre.Console; +using System.ComponentModel; +using System.Diagnostics; +using System.Globalization; +using System.Reflection; + +namespace CodingTracker; +internal static class Helpers +{ + private static readonly UserInterface userInterface = new(); + + internal static List ParseAnonObjToCodingSession(List listFromDB) + { + var list = new List(); + + foreach (var item in listFromDB) + { + var newCodingSession = new CodingSession() + { + Id = (int)item.Id, + StartTime = DateTime.ParseExact(item.StartTime, "yyyy-MM-dd HH:mm", new CultureInfo("en-US")), + EndTime = DateTime.ParseExact(item.EndTime, "yyyy-MM-dd HH:mm", new CultureInfo("en-US")), + Duration = TimeSpan.Parse(item.Duration), + }; + + + list.Add(newCodingSession); + } + + return list; + } + + internal static string GetDateInput(string message) + { + var dateInput = AnsiConsole.Prompt( + new TextPrompt(message)); + + if (dateInput.ToLower() == "t") return DateTime.Now.ToString("yyyy-MM-dd HH:mm"); + + while (!IsFormattedCorrectly(dateInput, "yyyy-MM-dd HH:mm") || String.IsNullOrEmpty(dateInput)) + { + dateInput = AnsiConsole.Prompt( + new TextPrompt("Invalid input, try again.")); + } + + return dateInput; + } + + internal static bool IsFormattedCorrectly(string date, string format) + { + if (!DateTime.TryParseExact(date, "yyyy-MM-dd HH:mm", new CultureInfo("en-US"), DateTimeStyles.None, out _)) + { + return false; + } + + return true; + } + + internal static bool IsEndTimeLowerThanStartTime(string startTime, string endTime) + { + var sT = DateTime.ParseExact(startTime, "yyyy-MM-dd HH:mm", new CultureInfo("en-US")); + + var eT = DateTime.ParseExact(endTime, "yyyy-MM-dd HH:mm", new CultureInfo("en-US")); + + return eT < sT; + } + + internal static string RunStopWatch() + { + AnsiConsole.MarkupLine(@"-------- TIME YOUR SESSION --------- + Press 'q' to quit. + Press 'p' to pause + Press 'r' to reset + "); + + Stopwatch stopwatch = new(); + stopwatch.Start(); + + bool isRunning = true; + bool isPaused = false; + + while (isRunning) + { + if (Console.KeyAvailable) + { + ConsoleKeyInfo keyInfo = Console.ReadKey(true); + + switch (keyInfo.Key) + { + case ConsoleKey.P: + if (isPaused) + { + stopwatch.Start(); + isPaused = false; + } + else + { + stopwatch.Stop(); + isPaused = true; + } + break; + + case ConsoleKey.Q: + isRunning = false; + stopwatch.Stop(); + break; + + case ConsoleKey.R: + stopwatch.Restart(); + break; + } + } + var time = stopwatch.Elapsed; + Console.SetCursorPosition(0, Console.CursorTop); + AnsiConsole.Markup($"Time: {time.ToString(@"hh\:mm\:ss")}"); + Thread.Sleep(50); + } + + var endTime = DateTime.Now.ToString("yyyy-MM-dd HH:mm"); + AnsiConsole.MarkupLine("\nSession finished.\nPress any key to continue."); + Console.ReadKey(); + + return endTime; + } + + internal static void CreateTable(List list, string[]? arr = null) + { + var tableData = new List>(); + + foreach (var item in list) + { + var row = new List + { + item.Id, + item.StartTime.ToString("dd-MMM-yyyy HH:mm tt"), + item.EndTime.ToString("dd-MMM-yyyy HH:mm tt"), + $"{(int)item.Duration.TotalHours}h {item.Duration.Minutes}m" + }; + + tableData.Add(row); + } + + ConsoleTableBuilder + .From(tableData) + .WithColumn(arr) + .WithFormat(ConsoleTableBuilderFormat.Alternative) + .WithTitle("Your report",ConsoleColor.DarkYellow) + .ExportAndWriteLine(); + } + + internal static void CreateTableOfAvg(List list) + { + var total = TimeSpan.Zero; + var count = list.Count; + + foreach (var item in list) + { + total += TimeSpan.Parse(item); + } + + var avg = total / count; + + var newObject = new List() + { + new + { + NumOfSessions = count, + Total = $"{(int)total.TotalHours}h {total.Minutes}m", + Avg = $"{(int)avg.TotalHours}h {avg.Minutes}m" + } + + }; + + ConsoleTableBuilder + .From(newObject) + .WithColumn("Num of Sessions", "Total", "Average per day") + .WithFormat(ConsoleTableBuilderFormat.Alternative) + .WithTitle("-------------") + .ExportAndWriteLine(); + } + + internal static void CheckIfListIsNullOrEmpty(List? list) + { + if (list == null || list.Count == 0) + { + AnsiConsole.MarkupLine("[red]No data found.[/]"); + AnsiConsole.MarkupLine("Press Any Key to Continue."); + Console.ReadKey(); + return; + } + } + + internal static void DisplayMessage(string message, string color = "blue") + { + AnsiConsole.MarkupLine($"[{color}]{message}[/]"); + } + + internal static bool ConfirmDeletion(string itemName) + { + var confrim = AnsiConsole.Confirm($"Are you sure you want to delete {itemName}?"); + + return confrim; + } + + internal static string GetDescription(Enum value) + { + var field = value.GetType().GetField(value.ToString()); + + var attr = field?.GetCustomAttribute(); + + return attr?.Description ?? value.ToString(); + } +} diff --git a/santisica29.CodingTracker/CodingTracker/Models/CodingSession.cs b/santisica29.CodingTracker/CodingTracker/Models/CodingSession.cs new file mode 100644 index 00000000..e13ff88e --- /dev/null +++ b/santisica29.CodingTracker/CodingTracker/Models/CodingSession.cs @@ -0,0 +1,27 @@ +namespace CodingTracker.Models; +public class CodingSession +{ + public int Id { get; set; } + public DateTime StartTime { get; set; } + public DateTime EndTime { get; set; } + public TimeSpan Duration { get; set; } + + public CodingSession() + { + + } + public CodingSession(DateTime startTime, DateTime endTime) + { + StartTime = startTime; + EndTime = endTime; + Duration = CalculateDuration(); + } + internal TimeSpan CalculateDuration() + { + if (EndTime < StartTime) + { + throw new ArgumentException("EndTime cannot be earlier than StartTime."); + } + return EndTime - StartTime; + } +} diff --git a/santisica29.CodingTracker/CodingTracker/Program.cs b/santisica29.CodingTracker/CodingTracker/Program.cs new file mode 100644 index 00000000..4b893ee8 --- /dev/null +++ b/santisica29.CodingTracker/CodingTracker/Program.cs @@ -0,0 +1,10 @@ +using CodingTracker.View; +using CodingTracker.Data; + +DatabaseInitializer.CreateDatabase(); +UserInterface userInterface = new(); +userInterface.MainMenu(); + + + + diff --git a/santisica29.CodingTracker/CodingTracker/Properties/launchSettings.json b/santisica29.CodingTracker/CodingTracker/Properties/launchSettings.json new file mode 100644 index 00000000..efa2499b --- /dev/null +++ b/santisica29.CodingTracker/CodingTracker/Properties/launchSettings.json @@ -0,0 +1,8 @@ +{ + "profiles": { + "CodingTracker": { + "commandName": "Project", + "workingDirectory": "C:\\Users\\Santi\\Desktop\\c#\\C#AcademyForks\\CodeReviews.Console.CodingTracker\\santisica29.CodingTracker\\CodingTracker" + } + } +} \ No newline at end of file diff --git a/santisica29.CodingTracker/CodingTracker/View/UserInterface.cs b/santisica29.CodingTracker/CodingTracker/View/UserInterface.cs new file mode 100644 index 00000000..bee24fa9 --- /dev/null +++ b/santisica29.CodingTracker/CodingTracker/View/UserInterface.cs @@ -0,0 +1,53 @@ +using CodingTracker.Controller; +using Spectre.Console; +using static CodingTracker.Enums; + +namespace CodingTracker.View; +internal class UserInterface +{ + private readonly CodingController _codingController = new(); + + internal void MainMenu() + { + bool flag = true; + while (flag) + { + Console.Clear(); + + var choice = AnsiConsole.Prompt( + new SelectionPrompt() + .Title("CODING TRACKER") + .UseConverter(e => Helpers.GetDescription(e)) + .AddChoices(Enum.GetValues())); + + switch (choice) + { + case MenuOption.AddCodingSession: + _codingController.AddSession(); + break; + case MenuOption.ViewCodingSession: + _codingController.ViewSessions(_codingController.GetSessions()); + break; + case MenuOption.DeleteCodingSession: + _codingController.DeleteSession(); + break; + case MenuOption.UpdateCodingSession: + _codingController.UpdateSession(); + break; + case MenuOption.StartSession: + _codingController.StartSession(); + break; + case MenuOption.ViewReportOfCodingSession: + _codingController.ViewReportOfCodingSession(); + break; + case MenuOption.Exit: + AnsiConsole.MarkupLine("Goodbye"); + flag = false; + break; + default: + AnsiConsole.MarkupLine("Invalid input"); + break; + } + } + } +} diff --git a/santisica29.CodingTracker/CodingTracker/coding_tracker.db b/santisica29.CodingTracker/CodingTracker/coding_tracker.db new file mode 100644 index 00000000..1d93b2c2 Binary files /dev/null and b/santisica29.CodingTracker/CodingTracker/coding_tracker.db differ diff --git a/santisica29.CodingTracker/README.md b/santisica29.CodingTracker/README.md new file mode 100644 index 00000000..b13901e3 --- /dev/null +++ b/santisica29.CodingTracker/README.md @@ -0,0 +1,45 @@ +# Coding Tracker +My fourth C# application following [The C# Sharp Academy Path](https://www.thecsharpacademy.com/#console-area) and my last beginner Console app. + +Based on the [coding tracker](https://www.thecsharpacademy.com/project/13/coding-tracker) project. +Console based CRUD application to track time spent coding. +Developed using C#, SQLite, Dapper ORM and [ConsoleTableExt Library](https://github.com/minhhungit/ConsoleTableExt). + +# Given Requirements: +- [x] When the application starts, it should create a sqlite database, if one isn’t present. +- [x] It should also create a table in the database, where the hours will be logged. +- [x] You need to be able to insert, delete, update and view your logged hours. +- [x] You should handle all possible errors so that the application never crashes (Dates and Times) +- [x] The application should only be terminated when the user inserts 0. +- [x] You can only interact with the database using SQLite and Dapper ORM. +- [x] You have to use Separation of Concerns to keep your code clean and organize. +- [x] You should adhere to the DRY principle when you can . + +# 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 they will be created on program start. + +* CRUD DB functions + + - From the main menu users can Create, Read, Update or Delete entries for whichever date they want, entered in yyyy-MM-dd HH:mm format. + - Time and Dates inputted are checked to make sure they are in the correct and realistic format. + +* Reporting and other data output uses ConsoleTableExt library to output in a more pleasant way + + - ![image](https://user-images.githubusercontent.com/15159720/141688462-e5dc465c-f188-4ac9-a166-397653c53c41.png) + +# Challenges + +- It was my first time using Dapper ORM and ConsoleTableExt. I had to learn how to use these technologies in order to complete this project. +- Dapper was easier than using ADO.NET but still I had to learn how to recieve the list in that format and parse them back to the type of list of my liking. +- Being able to start a session (pause, restart and finish) in the console and then choosing if you want to add it to the database. +- Filter the coding records per period (days, weeks, years). +- Creating a report showing the total of the duration as well as the average duration per day. + +# Areas to Improve +- I need to get better at using the same methods for different purposes, I think i did an okey job by using a lot of default parameters but it can be improved. +- Single responsibility. I did improve at this along the way, but I still have some work to do on it. I think I could have made my methods a little better so they only had a single use. +- I need to get better at naming my methods, sometimes I don't know if my code it's readable enough for others programers. \ No newline at end of file