From fb25976408bacb2b43a410bf4745378887f15cf4 Mon Sep 17 00:00:00 2001
From: AaronjMurphy <156782950+AaronjMurphy@users.noreply.github.com>
Date: Fri, 23 Oct 2026 14:36:59 +0100
Subject: [PATCH] Complete project development
---
CodingTracker/.gitattributes | 63 +++
CodingTracker/.gitignore | 363 ++++++++++++++++++
CodingTracker/CodingTracker.slnx | 3 +
.../CodingTracker/CodingTracker.csproj | 29 ++
.../Controllers/CodingTrackerController.cs | 217 +++++++++++
.../Data/CodingSessionRepository.cs | 124 ++++++
.../CodingTracker/Data/ConfigReader.cs | 18 +
.../CodingTracker/Data/DatabaseInitialiser.cs | 55 +++
.../CodingTracker/Enums/SortOrderOptions.cs | 8 +
.../CodingTracker/Enums/TrackingType.cs | 8 +
.../CodingTracker/Models/CodingSession.cs | 10 +
CodingTracker/CodingTracker/Program.cs | 17 +
CodingTracker/CodingTracker/README.md | 75 ++++
.../CodingTracker/UI/TablePrinter.cs | 49 +++
CodingTracker/CodingTracker/UI/UserInput.cs | 130 +++++++
CodingTracker/CodingTracker/appsettings.json | 5 +
16 files changed, 1174 insertions(+)
create mode 100644 CodingTracker/.gitattributes
create mode 100644 CodingTracker/.gitignore
create mode 100644 CodingTracker/CodingTracker.slnx
create mode 100644 CodingTracker/CodingTracker/CodingTracker.csproj
create mode 100644 CodingTracker/CodingTracker/Controllers/CodingTrackerController.cs
create mode 100644 CodingTracker/CodingTracker/Data/CodingSessionRepository.cs
create mode 100644 CodingTracker/CodingTracker/Data/ConfigReader.cs
create mode 100644 CodingTracker/CodingTracker/Data/DatabaseInitialiser.cs
create mode 100644 CodingTracker/CodingTracker/Enums/SortOrderOptions.cs
create mode 100644 CodingTracker/CodingTracker/Enums/TrackingType.cs
create mode 100644 CodingTracker/CodingTracker/Models/CodingSession.cs
create mode 100644 CodingTracker/CodingTracker/Program.cs
create mode 100644 CodingTracker/CodingTracker/README.md
create mode 100644 CodingTracker/CodingTracker/UI/TablePrinter.cs
create mode 100644 CodingTracker/CodingTracker/UI/UserInput.cs
create mode 100644 CodingTracker/CodingTracker/appsettings.json
diff --git a/CodingTracker/.gitattributes b/CodingTracker/.gitattributes
new file mode 100644
index 000000000..1ff0c4230
--- /dev/null
+++ b/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/CodingTracker/.gitignore b/CodingTracker/.gitignore
new file mode 100644
index 000000000..9491a2fda
--- /dev/null
+++ b/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/CodingTracker/CodingTracker.slnx b/CodingTracker/CodingTracker.slnx
new file mode 100644
index 000000000..c4e83d0e1
--- /dev/null
+++ b/CodingTracker/CodingTracker.slnx
@@ -0,0 +1,3 @@
+
+
+
diff --git a/CodingTracker/CodingTracker/CodingTracker.csproj b/CodingTracker/CodingTracker/CodingTracker.csproj
new file mode 100644
index 000000000..ce2ce1a4e
--- /dev/null
+++ b/CodingTracker/CodingTracker/CodingTracker.csproj
@@ -0,0 +1,29 @@
+
+
+
+ Exe
+ net10.0
+ enable
+ enable
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Always
+
+
+
+
diff --git a/CodingTracker/CodingTracker/Controllers/CodingTrackerController.cs b/CodingTracker/CodingTracker/Controllers/CodingTrackerController.cs
new file mode 100644
index 000000000..88136ff23
--- /dev/null
+++ b/CodingTracker/CodingTracker/Controllers/CodingTrackerController.cs
@@ -0,0 +1,217 @@
+using CodingTracker.Data;
+using CodingTracker.Enums;
+using CodingTracker.Models;
+using CodingTracker.UI;
+
+namespace CodingTracker.Controllers
+{
+ internal class CodingTrackerController
+ {
+ internal void Run()
+ {
+ bool exitApp = false;
+ while (!exitApp)
+ {
+ UserInput.ClearDisplay();
+ UserInput.DisplayMainMenuFiglet();
+
+ string mMenuOption = UserInput.GetMainMenuInput();
+
+ switch (mMenuOption)
+ {
+ case "Start Coding Session":
+ StartCodingSession();
+ break;
+ case "Manage Coding Sessions":
+ ManageSessions();
+ break;
+ case "Exit":
+ exitApp = true;
+ break;
+ }
+ }
+ }
+
+ private static void StartCodingSession()
+ {
+ DateTime startDate = DateTime.Now;
+ DateTime endDate = DateTime.Now;
+ TimeSpan duration = TimeSpan.Zero;
+ if (!UserInput.Confirm("Are you sure you would like to start a coding session?"))
+ {
+ UserInput.Continue();
+ return;
+ }
+
+ if (UserInput.GetTrackingType() == TrackingType.AutoTimer)
+ {
+ UserInput.DisplayMessage("The timer has started!");
+ startDate = DateTime.Now;
+ UserInput.ClearDisplay();
+ UserInput.DisplayMessage("Press 'Enter' when you wish to stop the timer");
+ UserInput.AwaitUserInput();
+ endDate = DateTime.Now;
+ duration = endDate - startDate;
+ }
+
+ else
+ {
+ (startDate, endDate, duration) = GetValidatedSessionTimeFrame();
+ }
+
+ CodingSession session = MapToCodingSession(startDate, endDate, duration);
+
+ if (!UserInput.Confirm("Are you sure you would like to add this session to the tracker?"))
+ {
+ UserInput.Continue();
+ return;
+ }
+
+ CodingSessionRepository.AddCodingSession(session);
+ UserInput.ConfirmationMessage("Session has been added!");
+ UserInput.Continue();
+ }
+
+ private static void ManageSessions()
+ {
+ bool exitToMainMenu = false;
+
+ while (!exitToMainMenu)
+ {
+ string menuOption = UserInput.GetManageSessionMenuOption();
+
+ switch (menuOption)
+ {
+ case "View All Sessions":
+ ViewAllSessions(CodingSessionRepository.GetAllSessions());
+ break;
+ case "Delete Session":
+ DeleteSession();
+ break;
+ case "Update Session":
+ UpdateSession();
+ break;
+ case "Exit to Main Menu":
+ exitToMainMenu = true;
+ break;
+ }
+ }
+ }
+
+ private static void ViewAllSessions(List sessions)
+ {
+ if (sessions.Count == 0)
+ {
+ UserInput.Error("There are no sessions to view.");
+ UserInput.Continue();
+ return;
+ }
+
+ TablePrinter.PrintSessionsTable(sessions);
+ UserInput.Continue();
+ }
+
+ private static void DeleteSession()
+ {
+ List sessionIds = CodingSessionRepository.GetAllSessionIds();
+
+ if (sessionIds.Count == 0)
+ {
+ UserInput.Error("There are no sessions to delete.");
+ UserInput.Continue();
+ return;
+ }
+
+ int sessionId = UserInput.GetCodingSessionIdInput(sessionIds);
+ if (!UserInput.Confirm($"Are you sure you would like to delete coding session {sessionId} from the tracker?"))
+ {
+ UserInput.ConfirmationMessage("Session deletetion cancelled.");
+ UserInput.Continue();
+ return;
+ }
+
+ ExecuteSessionDeletion(sessionId);
+ UserInput.ConfirmationMessage("Session has been deleted!");
+ UserInput.Continue();
+ }
+
+ private static void ExecuteSessionDeletion(int sessionId)
+ {
+ CodingSessionRepository.DeleteSessionFromId(sessionId);
+ }
+
+ private static void UpdateSession()
+ {
+ List sessionIds = CodingSessionRepository.GetAllSessionIds();
+
+ if (sessionIds.Count == 0)
+ {
+ UserInput.Error("There are no sessions to update.");
+ UserInput.Continue();
+ return;
+ }
+
+ int sessionId = UserInput.GetCodingSessionIdInput(sessionIds);
+ CodingSession session = CodingSessionRepository.GetSessionFromId(sessionId);
+ CodingSession updatedSession = GetCodingSessionUpdateInput(session);
+ if (!UserInput.Confirm($"Are you sure you would like to update coding session {sessionId} with these details?"))
+ {
+ UserInput.ConfirmationMessage("Session update cancelled.");
+ UserInput.Continue();
+ return;
+ }
+
+ ExecuteUpdateSession(sessionId, updatedSession);
+ UserInput.ConfirmationMessage("Session has been updated!");
+ UserInput.Continue();
+ }
+
+ private static void ExecuteUpdateSession(int sessionId, CodingSession updatedSession)
+ {
+ CodingSessionRepository.UpdateSessionFromId(sessionId, updatedSession);
+ }
+
+ internal static CodingSession GetCodingSessionUpdateInput(CodingSession session)
+ {
+ DateTime startTime;
+ DateTime endTime;
+ TimeSpan duration;
+
+ (startTime, endTime, duration) = GetValidatedSessionTimeFrame();
+
+
+ return MapToCodingSession(startTime, endTime, duration, session.ID);
+ }
+
+ private static (DateTime start, DateTime end, TimeSpan duration) GetValidatedSessionTimeFrame()
+ {
+ while (true)
+ {
+ DateTime startTime = UserInput.GetDateTime("start");
+ DateTime endTime = UserInput.GetDateTime("end");
+
+ if (endTime > startTime)
+ {
+ return (startTime, endTime, endTime - startTime);
+ }
+
+ UserInput.Error("Start date cannot be greater than end date! Please try again.");
+ UserInput.Continue();
+ }
+ }
+
+ private static CodingSession MapToCodingSession(DateTime start, DateTime end, TimeSpan duration, int Id = 0)
+ {
+ int totalHours = (int)duration.TotalHours;
+ int minutes = duration.Minutes;
+
+ return new CodingSession
+ {
+ ID = Id,
+ StartTime = start.ToString("yyyy-MM-dd HH:mm"),
+ EndTime = end.ToString("yyyy-MM-dd HH:mm"),
+ Duration = $"{totalHours:D2}:{minutes:D2}"
+ };
+ }
+ }
+}
diff --git a/CodingTracker/CodingTracker/Data/CodingSessionRepository.cs b/CodingTracker/CodingTracker/Data/CodingSessionRepository.cs
new file mode 100644
index 000000000..e4e4c0670
--- /dev/null
+++ b/CodingTracker/CodingTracker/Data/CodingSessionRepository.cs
@@ -0,0 +1,124 @@
+using CodingTracker.Models;
+using Dapper;
+using Microsoft.Data.Sqlite;
+namespace CodingTracker.Data
+{
+ internal class CodingSessionRepository
+ {
+ private static string connectionString = ConfigReader.ReadConfig();
+ internal static void AddCodingSession(CodingSession session)
+ {
+ try
+ {
+ var sql = @"INSERT INTO CodingSessions (StartTime, EndTime, Duration)
+ VALUES (@StartTime, @EndTime, @Duration)";
+
+ using var connection = new SqliteConnection(connectionString);
+ connection.Open();
+
+ connection.Execute(sql, session);
+
+ }
+ catch (Exception)
+ {
+ throw;
+ }
+
+ }
+
+ internal static List GetAllSessions()
+ {
+ List sessions = new List();
+ try
+ {
+ var sql = @"SELECT * FROM CodingSessions;";
+
+ using var connection = new SqliteConnection(connectionString);
+ connection.Open();
+
+ sessions = connection.Query(sql).ToList();
+ }
+ catch (Exception)
+ {
+ throw;
+ }
+
+ return sessions;
+ }
+
+ internal static List GetAllSessionIds()
+ {
+ List sessionIds = new List();
+ try
+ {
+ var sql = @"SELECT ID FROM CodingSessions;";
+
+ using var connection = new SqliteConnection(connectionString);
+ connection.Open();
+
+ sessionIds = connection.Query(sql).ToList();
+ }
+ catch (Exception)
+ {
+ throw;
+ }
+
+ return sessionIds;
+ }
+
+ internal static CodingSession GetSessionFromId(int id)
+ {
+ try
+ {
+ var sql = @"SELECT * FROM CodingSessions
+ WHERE ID = @ID;";
+
+ using var connection = new SqliteConnection(connectionString);
+ connection.Open();
+
+ return connection.QuerySingleOrDefault(sql, new { ID = id });
+ }
+ catch (Exception)
+ {
+ throw;
+ }
+ }
+
+ internal static void DeleteSessionFromId(int id)
+ {
+ try
+ {
+ var sql = @"DELETE FROM CodingSessions
+ WHERE ID = @ID;";
+
+ using var connection = new SqliteConnection(connectionString);
+ connection.Open();
+
+ connection.Execute(sql, new {ID = id});
+ }
+ catch (Exception)
+ {
+ throw;
+ }
+ }
+
+ internal static void UpdateSessionFromId(int id, CodingSession session)
+ {
+ try
+ {
+ var sql = @"UPDATE CodingSessions
+ SET StartTime = @StartTime, EndTime = @EndTime, Duration = @Duration
+ WHERE ID = @ID;";
+
+ using var connection = new SqliteConnection(connectionString);
+ connection.Open();
+
+ connection.Execute(sql, session);
+ }
+ catch (Exception)
+ {
+ throw;
+ }
+ }
+ }
+}
diff --git a/CodingTracker/CodingTracker/Data/ConfigReader.cs b/CodingTracker/CodingTracker/Data/ConfigReader.cs
new file mode 100644
index 000000000..c8b333244
--- /dev/null
+++ b/CodingTracker/CodingTracker/Data/ConfigReader.cs
@@ -0,0 +1,18 @@
+using Microsoft.Extensions.Configuration;
+
+namespace CodingTracker.Data
+{
+ internal class ConfigReader
+ {
+ internal static string ReadConfig()
+ {
+ string? connectionString = new ConfigurationBuilder()
+ .AddJsonFile("appsettings.json")
+ .Build()
+ .GetConnectionString("DefaultConnection")
+ ?? throw new InvalidOperationException("Key 'DefaultConnection' was not found.");
+
+ return connectionString;
+ }
+ }
+}
diff --git a/CodingTracker/CodingTracker/Data/DatabaseInitialiser.cs b/CodingTracker/CodingTracker/Data/DatabaseInitialiser.cs
new file mode 100644
index 000000000..af123bffd
--- /dev/null
+++ b/CodingTracker/CodingTracker/Data/DatabaseInitialiser.cs
@@ -0,0 +1,55 @@
+using Microsoft.Data.Sqlite;
+using Dapper;
+using CodingTracker.Models;
+
+namespace CodingTracker.Data
+{
+ internal class DatabaseInitialiser
+ {
+ internal static void InitialiseDatabase()
+ {
+ try
+ {
+ string connectionString = ConfigReader.ReadConfig();
+
+ using var connection = new SqliteConnection(connectionString);
+ connection.Open();
+
+ connection.Execute(@"
+ CREATE TABLE IF NOT EXISTS CodingSessions (
+ ID INTEGER PRIMARY KEY AUTOINCREMENT,
+ StartTime TEXT NOT NULL,
+ EndTime TEXT NOT NULL,
+ Duration TEXT NOT NULL);");
+
+ SeedData(connection);
+ }
+ catch (Exception)
+ {
+ throw;
+ }
+ }
+
+ internal static void SeedData(SqliteConnection connection)
+ {
+ int recordCount = connection.ExecuteScalar("SELECT COUNT(*) FROM CodingSessions;");
+
+ if (recordCount > 0)
+ return;
+
+ List seedSessions = new List
+ {
+ new() { ID = 0, StartTime = "2026-10-15 09:00", EndTime = "2026-10-15 11:30", Duration = "02:30" },
+ new() { ID = 0, StartTime = "2026-10-16 14:15", EndTime = "2026-10-16 15:45", Duration = "01:30" },
+ new() { ID = 0, StartTime = "2026-10-17 19:00", EndTime = "2026-10-17 22:15", Duration = "03:15" },
+ new() { ID = 0, StartTime = "2026-10-18 08:30", EndTime = "2026-10-18 10:00", Duration = "01:30" },
+ new() { ID = 0, StartTime = "2026-10-19 11:00", EndTime = "2026-10-19 16:45", Duration = "05:45" }
+ };
+
+ string sql = @"INSERT INTO CodingSessions (StartTime, EndTime, Duration)
+ VALUES (@StartTime, @EndTime, @Duration);";
+
+ connection.Execute(sql, seedSessions);
+ }
+ }
+}
diff --git a/CodingTracker/CodingTracker/Enums/SortOrderOptions.cs b/CodingTracker/CodingTracker/Enums/SortOrderOptions.cs
new file mode 100644
index 000000000..c4109fd0a
--- /dev/null
+++ b/CodingTracker/CodingTracker/Enums/SortOrderOptions.cs
@@ -0,0 +1,8 @@
+namespace CodingTracker.Enums
+{
+ internal enum SortOrderOptions
+ {
+ Ascending,
+ Descending
+ }
+}
diff --git a/CodingTracker/CodingTracker/Enums/TrackingType.cs b/CodingTracker/CodingTracker/Enums/TrackingType.cs
new file mode 100644
index 000000000..0a9dc0f6f
--- /dev/null
+++ b/CodingTracker/CodingTracker/Enums/TrackingType.cs
@@ -0,0 +1,8 @@
+namespace CodingTracker.Enums
+{
+ internal enum TrackingType
+ {
+ Manual,
+ AutoTimer
+ }
+}
diff --git a/CodingTracker/CodingTracker/Models/CodingSession.cs b/CodingTracker/CodingTracker/Models/CodingSession.cs
new file mode 100644
index 000000000..1b3d80d33
--- /dev/null
+++ b/CodingTracker/CodingTracker/Models/CodingSession.cs
@@ -0,0 +1,10 @@
+namespace CodingTracker.Models
+{
+ internal class CodingSession
+ {
+ public required int ID { get; set; }
+ public required string StartTime { get; set; }
+ public required string EndTime { get; set; }
+ public required string Duration { get; set; }
+ }
+}
diff --git a/CodingTracker/CodingTracker/Program.cs b/CodingTracker/CodingTracker/Program.cs
new file mode 100644
index 000000000..75d492d37
--- /dev/null
+++ b/CodingTracker/CodingTracker/Program.cs
@@ -0,0 +1,17 @@
+using CodingTracker.Controllers;
+using CodingTracker.Data;
+using CodingTracker.UI;
+
+namespace CodingTracker
+{
+ internal class Program
+ {
+ static void Main(string[] args)
+ {
+ DatabaseInitialiser.InitialiseDatabase();
+ var app = new CodingTrackerController();
+ app.Run();
+ UserInput.Goodbye();
+ }
+ }
+}
diff --git a/CodingTracker/CodingTracker/README.md b/CodingTracker/CodingTracker/README.md
new file mode 100644
index 000000000..e1bb88c6d
--- /dev/null
+++ b/CodingTracker/CodingTracker/README.md
@@ -0,0 +1,75 @@
+
+# Coding Tracker Project
+
+This project is a coding tracker based on requirements from The C# Academy. It allows a user to track their coding sessions, which are then added to a database. The user can decide between entering the details manually or using a timer to track their sessions This database can be read from and its data can be displayed. Added coding sessions can be removed and updated by the user.
+
+## How to Run the Project
+
+This project uses several exteral libraries and extensions for styling console text and creating tables, reading data, mapping objects to database entries and allowing the execution of sql commands.
+
+To install these dependencies use these commands in the projects folder accessed in the terminal:
+
+- 'dotnet add package Spectre.Console' - Used for styling console text and creating tables.
+- 'dotnet add package Microsoft.Data.Sqlite' - Allows the use of sql commands and database connection
+- 'dotnet add package Dapper' - The ORM. This maps database entries to objects.
+- 'dotnet add package Microsoft.Extensions.Configuration' - This allows us to read JSON files.
+
+## How it Works
+
+On starting the app the user will be presented with a title and a list of options including: 'Start Coding Session', 'Manage Coding Sessions' and 'Exit'.
+
+### Start Coding Session
+On selecting this option the user will be asked to confirm that they wish to start tracking a coding session. If the enters 'n', then they will be returned to the previous menu, else they will be prompted to choose between tracking a session manually or using a timer. Choosing the 'Manual' option will prompt the user to enter the date and time for the start and end of the coding session. If a user selects 'AutoTimer', a timer will begin and will end when the user presses 'Enter'. Once a session has been completed and entered the user is asked to confirm that they wish to store it in the tracker.
+
+### Manage Coding Sessions
+On selecting this option the user will be prompted with another menu with several options including: 'View All Sessions', "Delete Session", "Update Session" and 'Exit'.
+
+#### View All Sessions:
+Selecting this option will prompt the user to select whether they wish to view all sessions in ascending or descending order. Once selected a table with all sessions will be printed based on the preivous input.
+
+#### Delete Session:
+Selecting this option will prompt the user to select the ID of the session they wish to delete. Once selected the user will be asked to confirm that they wish to delete this session. If the user enters 'n' then they are returned to the preivous menu. Else the session will be deleted and a confirmation message will appear.
+
+#### Update Session:
+Selecting this option will prompt the user to select the ID of the session they wish to update. Once selected, more prompts will appear asking the user to enter the 'start time' as well as the 'end time' for the session. Once entered correctly the user will be asked to confirm if they wish to update the session. If they decline they will be returned to the preivous menu. Else the session will be updated and a confirmation message will appear.
+
+#### Exit to Main Menu
+Selection this option will return the user to the main menu.
+
+### Exit
+
+On selecting this option a message will appear thanking the user for using the application. They will be prompted to press 'Enter' and the app will subsequently close.
+
+## The Creation Process
+
+The first feature I decided to tackle was to create a main menu for the user to land on when launching the app. This was done using a switch statement inside a Run() method in my controller class and the user input is gathered from the GetMainMenuInput() method inside of my UI class. Separating these two was necessary to adhere to SRP (or Single Responsibility Principle). Each class would handle its own related tasks. No functionality was added just yet, besides the ability to exit the app with the 'Exit' option.
+
+Next it was necessary to create a database file to read from and write to. To do this I created a new class called DatabaseInitialiser. This file uses 2 external libraries: Dapper and Microsoft.Data.Sqlite. Before writing the code to initialse my database I needed to create a JSON file that would store my connection string. To read from this, it was necessary to create a new class called ConfigReader. This file contains one method called ReadConfig(). This method reads from the database and gets the connection string, returning it when called. With this created it was now time to finish initialsing my database. I now create a string that calls the ConfigReader() method, using it as the connectionString. I then execute an sql command using Dapper that creates a table called CodingSessions if one does not exist already. When this is executed, a database file is created automatically. Both reading the config file and using database commands are very susceptible to issues so I felt it was necessary to use exception handling to ensure a clean exit from the app should a problem occur. The database is initialised once the app starts running.
+
+The next step was to allow the user to add a CodingSession to the database. To do this I created a CodingSession class. Each CodingSession object requires an ID, StartTime, EndTime and a Duration. In my CodingSessionRepository class I created a method called AddCodingSession(). This method attempts to connect to a database, therefore I used try-catch blocks to ensure a clean exit should an error occur. First I create my sql command text and a connection string (aqcuired by calling the ReadConfig() method). I creat the connection using my connection string and open the connection. I then execute my sql command and pass in a CodingSessionObject. In my UserInput class I created a method called GetDateTime which takes a string as parameter that alters text depending on if it is the start or end time that is required. The user is prompted to input the date and time of the session, it is validated and returned. I also created some methods to prompt the user for confirmation, print an error message and to prompt the user to press enter to confirm. Back in CodingTrackerController I created a method called StartCodingSession(). This uses the confirm message mentioned earlier to ask the user if they are sure they wish to start a session. The GetDateTime() method is called to get both the startDate and endDate of the CodingSession. A check is also run to ensure that endDate is greater than startDate. If it is not then then an error is printed. A new CodingSession is created based on this input. Duration is calculated by removing the startDate from the endDate and then using the TotalHours and Minutes properties. The user is asked to confirm via a prompt that they wish to enter this CodingSession to the database. If they accept then the session is added by passing it to the AddCodingSession() method.
+
+I then decided to create the functionality to allow the user to view all currently added sessions in the database. To do this I created a ManageSessions() method in my Controller class. This is also a menu so it uses a loop. Before adding the ViewAllSessions()
+method I created another method inside CodingSessionRepository called GetAllSesssions(). I create a List that will contain CodingSession objects. Like before I create the sql command, connect to the database and this time i use a different method to map the each session row to CodingSession objects, and then add them to the List, using Dapper. This method returns the List of CodingSession objects. To print the table I created a new TablePrinter class in my UI folder. This contains a PrintSessionsTable() method that takes in a List of CodingSession objects as a parameter. A check is run on the passed List to ensure that it is not empty. If it is the user is returned to the previous menu. If the List is not empty a table is created with its columns. The List is looped over and a row is created for every CodingSession. The table is then printed. Back in the ViewAllSessions() method, the PrintSessionTable is called with the GetAllSesssions() method passed as a parameter. To access this option, and future 'Manage Coding Session'options, from the menu I created a GetManageSessionMenuOption() method in UserInput. This prompts the user to select a menu option.
+
+Next came the option to delete a session from the database. In my CodingSessionRepository I created another method called GetAllSesssionIds() which was resonsible for returning a List of all CodingSession IDs currently stored in the database using Dapper to execute the command. Also in this class I created a DeleteSessionFromId() method which takes an int as its parameter. This int will be the Id that is passed to the method for deletion. This connects to the database like the previous methods but this time uses paramterised input (the Id) to execute the sql deletion command. In my UserInput class I created a method called GetCodingSessionIdInput(). This prompts the user to select an Id based on the passed List. Back in my Controller class I create a DeleteSession() method. This creates a List calling the GetAllSesssionIds() method for its values. The session Id, that will be deleted, is aqcuired by calling the GetCodingSessionIdInput() method with the list of session ids passed as a parameter. The user is asked to confirm that they wish to delete the session from the database and if so the ExecuteSessionDeletion() method is called. Inside this method DeleteSessionFromId() is called with the Id passes as a parameter. Originally this was done using one method, however I separated them to ensure my app follows SRP. At this point I also decided to include a GoodBye() method in my UserInput class which prints a message when called in the Program class. This is called on exiting the app.
+
+For updating a session I decided to refactor how I validate the startTime and endTime. I created a new method called GetValidatedSessionTimeFrame(). This returnes the startTime, endTime and duration values. This method will only stop running when the user enters the dates for both entries in the correct format and if the starting time is not greater than the end time. This method is used in StartCodingSession() to ensure that there is no duplicate code, following the DRY Principle (Dont Repeat Yourself). Another helper method I decided to add to my Controller class was MapToCodingSession() which handles the creation of new variables based on duration (totalHours, minutes). It returns a new CodingSession object. I created another method called GetCodingSessionUpdateInput() which takes in a CodingSession (that is to be updated). This method calls the GetValidatedSessionTimeFrame() method and returns the validated input by calling MapToCodingSession() with the start time, end time, duration and session ID passed as parameters. In the UpdateSession() method I first create a List that calls GetAllSessionIds(). This is passed to GetCodingSessionIdInput() to get the Id of the object the user wishes to update. A new CodingSession object is created that gets its value by calling the GetSessionFromId() method, (a method inside the CodingSessionRepository class. That queries the database and returns a session from a parameterised Id). The UpdateSessionFromId() method takes an id, and a CodingSession as parameters. It behaves like the DeleteSessionFromId() method, this time using a command to update a row in the database. Back in the Controller class I created an UpdateSession() method that creates a List of ids using the GetAllSessionIds() method. An Id is collected using GetCodingSessionIdInput(), passing the List as a parameter. A CodingSession object is created using GetSessionFromId() and passing the Id as a parameter. A new CodingSession object is created using the GetCodingSessionUpdateInput() method, passsing the preivous CodingSession object as a parameter. If the user confirms that they wish to update this session then ExecuteUpdateSession() is called, using the id, and the updated session as a parameter.
+
+With the main CRUD operations completed I decided it was necessary to create seed data for the database. With data already added the user will be able to use all menu operations from the moment the app starts. In my DatabaseInitialiser class I created a new method called SeedData() which takes an SqliteConnection as a parameter(which allows us to use the connection from the InitialiseDatabase() method). A check is run to ensure that there is data to read from and if not the method is exited. A list of new CodingSession objects is created and are used to query the database using paramterised values. This method is called at the end of the InitialiseDatabase() method. It was at this point I decided to alter the ViewAllSessions() method to require a List parameter. This allows me to pass the List of sessions and ensure that there are sessions to print using a check. I added this check to UpdateSession() and DeleteSession()
+
+Next I decided to add styling to the app using Spectre Console features. I added colour to text to make it stand out, ensuring that the prompts, column names, error messages and confirmation messages are clearer to the user. I also added a Figlet to the main menu for aesthetic purposes.
+
+An issue that remained was when a user selects 'Start Coding Session' and enters 'n' at the prompt, the user is brought to the main menu, however the figlet is printed twice. I could not use spectre console in the Controller class as that would violate SRP, however I created a class called ClearDisplay() in the UserInput class. This method is called in the Controller class instead. I then decided to add confirmation messages to each action the user interacts with when altering the database to ensure that they can change their mind or correct a mistake.
+
+An extra feature I decided to include in the application was the ability for the user to sort the coding sessions table. To do this I created an enum called SortOrderOptions. It contained the values 'Ascending' and 'Descending'. In the UserInput class I created a new method called GetOrderOption(). This prompts the user to select one of the enum values which will then be used to sort the table. In the TablePrinter class I created a SortSessions() method that takes a List and a SortOrderOptions enum value. If the enum value is 'Ascending' then the list is ordered by ascending using LINQ operations. Else it is ordered by Descending.
+
+The final feature I decided to implement was the ability to track a session using a timer. The user would be able to select the option, upon which the timer starts. It then awaits further user input to stop the timer and creates a CodingSessionObject based on the tracked information. To allow the user to select which type they wish to use to track their session I first created an Enum called TrackingType that contained two values: 'Manual' and 'AutoTimer'. In the UserInput class I created a GetTrackingType() method that prompts the user to select between the two TrackingType values. I also created two other methods: DisplayMessage() and AwaitUserInput() which were responsible for displaying a message to the user (in this case to prompt the user to press enter when they wish to complete the tracking of the session) as well as allowing the user to press 'Enter' to complete the session. Back in the Controller class I altered the StartCodingSession() method to include these new methods and the code to track the session using a timer.
+
+After completing the project requirements I decided to make one small adjustment after some testing. The table was being sorted by the StartTime of a CodingSession, however if two or more sessions happen to occur on the same day they may not be in the right order. To fix this issue I decided to use the EndTime of a CodingSession for this case.
+
+## Key Takeaways
+
+Overall I found this project quite enjoyable and it tested my coding ability. My first experience with the ORM, or in this case the micro ORM, Dapper has been very positive. I came to understand the usefulness of ORMS in mapping objects to database elements and found it incredibly intuitive to use. I look forward to using Dapper or other ORMS in future projects. At the beginning of this project I felt somewhat confused as to how to properly separate the concerns of my code. Upon further research I gained greater understanding of the benefits of SRP and how to properly implement it. I now feel more confident in applying it to future projects.
+
+
+
diff --git a/CodingTracker/CodingTracker/UI/TablePrinter.cs b/CodingTracker/CodingTracker/UI/TablePrinter.cs
new file mode 100644
index 000000000..bd329082b
--- /dev/null
+++ b/CodingTracker/CodingTracker/UI/TablePrinter.cs
@@ -0,0 +1,49 @@
+using CodingTracker.Enums;
+using CodingTracker.Models;
+using Spectre.Console;
+
+namespace CodingTracker.UI
+{
+ internal class TablePrinter
+ {
+ internal static void PrintSessionsTable(List sessions)
+ {
+ UserInput.ClearDisplay();
+
+ SortOrderOptions order = UserInput.GetOrderOption();
+ List orderedSessions = SortSessions(sessions, order);
+
+ Table sessionsTable = new Table()
+ .Title("[blue bold]Tracked Sessions[/]")
+ .AddColumn("[lightgreen bold]ID[/]")
+ .AddColumn("[lightgreen bold]StartTime[/]")
+ .AddColumn("[lightgreen bold]EndTime[/]")
+ .AddColumn("[lightgreen bold]Duration[/]")
+ .BorderColor(Color.Grey50);
+
+ foreach (var session in orderedSessions)
+ {
+ sessionsTable.AddRow(
+ session.ID.ToString(),
+ Markup.Escape(session.StartTime),
+ Markup.Escape(session.EndTime),
+ session.Duration);
+ }
+
+ AnsiConsole.Write(sessionsTable);
+ }
+
+ internal static List SortSessions(List sessions, SortOrderOptions order)
+ {
+
+ if (order == SortOrderOptions.Ascending)
+ {
+ return sessions.OrderBy(session => session.EndTime).ToList();
+ }
+ else
+ {
+ return sessions.OrderByDescending(session => session.EndTime).ToList();
+ }
+ }
+ }
+}
diff --git a/CodingTracker/CodingTracker/UI/UserInput.cs b/CodingTracker/CodingTracker/UI/UserInput.cs
new file mode 100644
index 000000000..2518d3320
--- /dev/null
+++ b/CodingTracker/CodingTracker/UI/UserInput.cs
@@ -0,0 +1,130 @@
+using CodingTracker.Enums;
+using Spectre.Console;
+using System.Globalization;
+namespace CodingTracker.UI
+{
+ internal class UserInput
+ {
+ private static string[] MainMenuOptions = { "Start Coding Session", "Manage Coding Sessions", "Exit" };
+ private static string[] ManageSessionsMenuOptions = { "View All Sessions" ,"Delete Session" ,"Update Session" ,"Exit to Main Menu" };
+
+ internal static string GetMainMenuInput()
+ {
+ return AnsiConsole.Prompt(
+ new SelectionPrompt()
+ .Title("[white bold]What would you like to do?[/]")
+ .AddChoices(MainMenuOptions));
+ }
+
+ internal static DateTime GetDateTime(string startOrEnd)
+ {
+ const string requiredFormat = "yyyy-MM-dd HH:mm";
+ ClearDisplay();
+ string input = AnsiConsole.Prompt(
+ new TextPrompt($"[white bold]Enter the {startOrEnd} time[/] [blue bold](yyyy-MM-dd HH:mm)[/]: ")
+ .Validate(time =>
+ {
+ bool validDate = DateTime.TryParseExact(
+ time,
+ requiredFormat,
+ CultureInfo.InvariantCulture,
+ DateTimeStyles.None,
+ out _);
+
+ return validDate
+ ? ValidationResult.Success()
+ : ValidationResult.Error("[red italic]Invalid date format![/]");
+ }))
+ .Trim();
+
+ return DateTime.ParseExact(input, requiredFormat, CultureInfo.InvariantCulture);
+ }
+ internal static string GetManageSessionMenuOption()
+ {
+ ClearDisplay();
+
+ return AnsiConsole.Prompt(
+ new SelectionPrompt()
+ .Title("[White bold]Select an option to manage coding sessions.[/]")
+ .AddChoices(ManageSessionsMenuOptions));
+ }
+
+ internal static int GetCodingSessionIdInput(List sessionIds)
+ {
+ ClearDisplay();
+
+ return AnsiConsole.Prompt(
+ new SelectionPrompt()
+ .Title("[white bold]Select the session ID[/]:")
+ .AddChoices(sessionIds));
+
+ }
+
+ internal static SortOrderOptions GetOrderOption()
+ {
+ return AnsiConsole.Prompt(
+ new SelectionPrompt()
+ .Title("[white bold]Select the order you would like to view the coding sessions in[/]:")
+ .AddChoices(SortOrderOptions.Ascending, SortOrderOptions.Descending));
+ }
+
+ internal static TrackingType GetTrackingType()
+ {
+ return AnsiConsole.Prompt(
+ new SelectionPrompt()
+ .Title("Select the tracking type:")
+ .AddChoices(TrackingType.AutoTimer, TrackingType.Manual));
+ }
+
+ internal static void DisplayMessage(string message)
+ {
+ AnsiConsole.MarkupLine($"[White bold]{message}[/]");
+ }
+
+ internal static void AwaitUserInput()
+ {
+ Console.ReadLine();
+ }
+
+ internal static bool Confirm(string message)
+ {
+ return AnsiConsole.Confirm($"[White bold]{message}[/]");
+ }
+
+ internal static void ConfirmationMessage(string message)
+ {
+ AnsiConsole.MarkupLine($"[white bold]{message}[/]");
+ }
+
+ internal static void Continue()
+ {
+ AnsiConsole.MarkupLine("[grey]Press 'Enter' to continue.[/]");
+ Console.ReadKey();
+ }
+
+ internal static void Error(string message)
+ {
+ AnsiConsole.MarkupLine($"[red italic]{message}[/]");
+ }
+
+ internal static void Goodbye()
+ {
+ AnsiConsole.MarkupLine("[white bold]Thank you for using the Coding tracker app![/]");
+ Continue();
+ }
+
+ internal static void DisplayMainMenuFiglet()
+ {
+ var figlet = new FigletText("Coding Tracker")
+ .Color(Color.LightGreen);
+
+ AnsiConsole.Write(figlet);
+ }
+
+ internal static void ClearDisplay()
+ {
+ AnsiConsole.Clear();
+ }
+
+ }
+}
diff --git a/CodingTracker/CodingTracker/appsettings.json b/CodingTracker/CodingTracker/appsettings.json
new file mode 100644
index 000000000..460169ebd
--- /dev/null
+++ b/CodingTracker/CodingTracker/appsettings.json
@@ -0,0 +1,5 @@
+{
+ "ConnectionStrings": {
+ "DefaultConnection": "Data Source=CodingSessionDatabase.db"
+ }
+}
\ No newline at end of file