Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
22 changes: 22 additions & 0 deletions CodeReviews.Console.CodingTracker.sln
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@

Microsoft Visual Studio Solution File, Format Version 12.00
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "CodingTracker", "CodingTracker\CodingTracker.csproj", "{514F39B8-9759-4A04-918A-C2B2C4E12D0B}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "CodingTrackerTests", "CodingTrackerTests\CodingTrackerTests.csproj", "{5104A8EC-03BB-4927-9FFF-7B33E0132E44}"
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
Release|Any CPU = Release|Any CPU
EndGlobalSection
GlobalSection(ProjectConfigurationPlatforms) = postSolution
{514F39B8-9759-4A04-918A-C2B2C4E12D0B}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{514F39B8-9759-4A04-918A-C2B2C4E12D0B}.Debug|Any CPU.Build.0 = Debug|Any CPU
{514F39B8-9759-4A04-918A-C2B2C4E12D0B}.Release|Any CPU.ActiveCfg = Release|Any CPU
{514F39B8-9759-4A04-918A-C2B2C4E12D0B}.Release|Any CPU.Build.0 = Release|Any CPU
{5104A8EC-03BB-4927-9FFF-7B33E0132E44}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{5104A8EC-03BB-4927-9FFF-7B33E0132E44}.Debug|Any CPU.Build.0 = Debug|Any CPU
{5104A8EC-03BB-4927-9FFF-7B33E0132E44}.Release|Any CPU.ActiveCfg = Release|Any CPU
{5104A8EC-03BB-4927-9FFF-7B33E0132E44}.Release|Any CPU.Build.0 = Release|Any CPU
EndGlobalSection
EndGlobal
26 changes: 26 additions & 0 deletions CodingTracker/AppSettings.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
namespace CodingTracker;

using System;
using System.IO;
using System.Text.Json;

public class AppSettings
{
public string DbPath { get; set; } = string.Empty;

private static readonly string FilePath = Path.Combine(AppContext.BaseDirectory, "appsettings.json");

public static AppSettings LoadConfig()
{
if (!File.Exists(FilePath))
{
throw new FileNotFoundException($"Configuration file missing at: {FilePath}");
}

string jsonContent = File.ReadAllText(FilePath);

return JsonSerializer.Deserialize<AppSettings>(jsonContent)
?? throw new InvalidOperationException("Failed to deserialize configuration file.");
}

}
33 changes: 33 additions & 0 deletions CodingTracker/CodingSession.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
using System.ComponentModel.DataAnnotations.Schema;

namespace CodingTracker;

public class CodingSession
{
public long? Id { get; set; }
public DateTime StartTime { get; set; }
public DateTime? EndTime { get; set; }
public long? DurationTicks { get; set; }
// We use Duration to use TimeSpan in the code even if SQLite doesn't support it
[NotMapped]
public TimeSpan Duration
{
get => TimeSpan.FromTicks(DurationTicks ?? 0);
set => DurationTicks = value.Ticks;
}

public CodingSession() { }

public CodingSession(DateTime startTime, DateTime? endTime, TimeSpan duration, long? id = null)
{
Id = id;
StartTime = startTime;
EndTime = endTime;
Duration = duration;
}

public override string ToString()
{
return $@"Coding Session {{start: {StartTime}, end: {EndTime}, duration: {Duration} }}";
}
}
22 changes: 22 additions & 0 deletions CodingTracker/CodingTracker.csproj
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
<Project Sdk="Microsoft.NET.Sdk">

<PropertyGroup>
<OutputType>Exe</OutputType>
<TargetFramework>net10.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
</PropertyGroup>

<ItemGroup>
<None Include="..\appsettings.json">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</None>
</ItemGroup>

<ItemGroup>
<PackageReference Include="Dapper" Version="2.1.89" />
<PackageReference Include="Dapper.SqlBuilder" Version="2.1.89" />
<PackageReference Include="Microsoft.Data.Sqlite" Version="11.0.0-rc.1.26425.128" />
<PackageReference Include="Spectre.Console" Version="0.57.3-alpha.0.28" />
</ItemGroup>
</Project>
30 changes: 30 additions & 0 deletions CodingTracker/ConfigSettings.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
namespace CodingTracker;

using System;
using System.IO;
using System.Text.Json;

public class ConfigSettings
{
public string DbPath { get; set; } = string.Empty;
}

public static class ReadConfig
{
public static ConfigSettings Config { get; set; }

private static readonly string FilePath = Path.Combine(AppContext.BaseDirectory, "appsettings.json");

static ReadConfig()
{
if (!File.Exists(FilePath))
{
throw new FileNotFoundException($"Configuration file missing at: {FilePath}");
}

string jsonContent = File.ReadAllText(FilePath);

Config = JsonSerializer.Deserialize<ConfigSettings>(jsonContent)
?? throw new InvalidOperationException("Failed to deserialize configuration file.");
}
}
124 changes: 124 additions & 0 deletions CodingTracker/Controller.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,124 @@
namespace CodingTracker;

public static class Controller
{
public static DateTime MonthToShow = Utils.GetFirstDayMonth(DateTime.Now);
private static UserOption.PromptMenus _promptMenu = UserOption.PromptMenus.Main;
private static bool _endApp = false;

public static async Task Start()
{
do
{
var sessions = await DataBaseInterface.ViewCodingSessions(MonthToShow, Utils.NextMonth(MonthToShow));
var sessionsPerDay = AmountCodingPerDay(sessions);
sessionsPerDay = AddEmptyCodingSessionIfEmpty(sessionsPerDay, MonthToShow);

var previousMonth = Utils.PreviousMonth(MonthToShow);
var sessionsPrevious = await DataBaseInterface.ViewCodingSessions(previousMonth, MonthToShow);
var sessionsPreviousPerDay = AmountCodingPerDay(sessionsPrevious);
sessionsPreviousPerDay = AddEmptyCodingSessionIfEmpty(sessionsPreviousPerDay, previousMonth);

UserInterface.ClearAndTitle();
TableVisualisationEngine.ShowCalendarWithCaption(sessionsPreviousPerDay, sessionsPerDay);
await Prompt();

} while (!_endApp);
}

private static async Task Prompt()
{
switch (_promptMenu)
{
case UserOption.PromptMenus.Main:
var choiceMainMenu = UserInterface.MainMenuPrompt();
MainMenu(choiceMainMenu);
break;
case UserOption.PromptMenus.View:
var choiceViewMenu = UserInterface.ViewMenuPrompt();
ViewMenu(choiceViewMenu);
break;
case UserOption.PromptMenus.AddLive:
var (startDateLive, endDateLive) = UserInterface.AskDateRangeLive();
await AddMenu(startDateLive, endDateLive);
_promptMenu = UserOption.PromptMenus.Main;
break;
case UserOption.PromptMenus.Add:
var (startDate, endDate) = UserInterface.AskDateRange();
await AddMenu(startDate, endDate);
_promptMenu = UserOption.PromptMenus.Main;
break;
default:
throw new ArgumentOutOfRangeException($"Not valid promptMenu");
}
}

private static void MainMenu(UserOption.MainMenu choice)
{
switch (choice)
{
case UserOption.MainMenu.Insert:
_promptMenu = UserOption.PromptMenus.Add;
break;
case UserOption.MainMenu.InsertLive:
_promptMenu = UserOption.PromptMenus.AddLive;
break;
case UserOption.MainMenu.View:
_promptMenu = UserOption.PromptMenus.View;
break;
case UserOption.MainMenu.Quit:
_endApp = true;
break;
default:
throw new ArgumentOutOfRangeException(nameof(choice), choice, null);
}
}

private static void ViewMenu(UserOption.ViewMenu choice)
{
switch (choice)
{
case UserOption.ViewMenu.Previous:
MonthToShow = Utils.PreviousMonth(MonthToShow);
break;
case UserOption.ViewMenu.Next:
MonthToShow = Utils.NextMonth(MonthToShow);
break;
case UserOption.ViewMenu.Quit:
_promptMenu = UserOption.PromptMenus.Main;
break;
default:
throw new ArgumentOutOfRangeException(nameof(choice), choice, null);
}
}

private static async Task AddMenu(DateTime start, DateTime end)
{
var duration = Utils.Duration(start, end);
var codingSession = new CodingSession(start, end, duration);
bool confirmation = UserInterface.ConfirmationPrompt($"add {codingSession}");

if (confirmation) await DataBaseInterface.AddCodingSession(codingSession);
}

/// Aggregates coding sessions by calendar day, returning one session per day with summed durations
/// We take this postulate: Sessions that happens past midnight are attributed entirely to their start date
public static List<CodingSession> AmountCodingPerDay(List<CodingSession> codingSessions)
{
return codingSessions
.GroupBy(codingSession => codingSession.StartTime.Date) // Only take the day of the startTime
.Select(codingSession =>
new CodingSession(
codingSession.Key,
codingSession.Key.AddDays(1),
TimeSpan.FromTicks(codingSession.Sum(s => s.Duration.Ticks))))
.ToList();
}

private static List<CodingSession> AddEmptyCodingSessionIfEmpty(List<CodingSession> sessions, DateTime monthToShow)
{
if (sessions.Count > 0) return sessions;
return [new CodingSession(monthToShow, monthToShow, new TimeSpan(0))];
}

}
64 changes: 64 additions & 0 deletions CodingTracker/DataBaseInterface.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
using Dapper;
using Microsoft.Data.Sqlite;

namespace CodingTracker;

public static class DataBaseInterface
{
private static SqliteConnection? _dbConnection;
private static bool _isInitialized = false;

private static void InitializeDb()
{
if (_isInitialized && _dbConnection != null) return;
string dbPath = ReadConfig.Config.DbPath;
string connectionString = "Data Source=" + dbPath;
_dbConnection = new SqliteConnection(connectionString);
_dbConnection.Open();
CreateTable();
_isInitialized = true;
}

private static void CreateTable()
{
string createTableQuery = @"
CREATE TABLE IF NOT EXISTS CodingSessions (
Id INTEGER PRIMARY KEY AUTOINCREMENT,
StartTime TEXT NOT NULL,
EndTime TEXT NOT NULL,
DurationTicks INTEGER NOT NULL);";

using var command = new SqliteCommand(createTableQuery, _dbConnection);
command.ExecuteNonQuery();
}

public static async Task AddCodingSession(CodingSession codingSession)
{
InitializeDb();
if (_dbConnection == null) throw new Exception("Database connection couldn't initialize");

string addCodingSessionQuery = @"
INSERT INTO CodingSessions (StartTime, EndTime, DurationTicks)
VALUES (@StartTime, @EndTime, @DurationTicks);";

await _dbConnection.ExecuteAsync(addCodingSessionQuery, codingSession);
}

public static async Task<List<CodingSession>> ViewCodingSessions(DateTime? minStartDate = null, DateTime? maxStartDate = null)
{
InitializeDb();
if (_dbConnection == null) throw new Exception("Database connection couldn't initialize");

var conditions = new List<string>();
if (minStartDate.HasValue) conditions.Add("StartTime >= @MinDate");
if (maxStartDate.HasValue) conditions.Add("StartTime <= @MaxDate");

string whereClauses = conditions.Count > 0 ? " WHERE " + string.Join(" AND ", conditions) : "";

string viewCodingSessionsQuery = $"SELECT * FROM CodingSessions{whereClauses} ORDER BY StartTime;";

return (await _dbConnection.QueryAsync<CodingSession>(viewCodingSessionsQuery,
new { MinDate = minStartDate, MaxDate = maxStartDate })).ToList();
}

}
9 changes: 9 additions & 0 deletions CodingTracker/Program.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
namespace CodingTracker;

static class Program
{
public static async Task Main(string[] args)
{
await Controller.Start();
}
}
Loading
Loading