Skip to content
Open
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
482 changes: 482 additions & 0 deletions CodingTracker/.gitignore

Large diffs are not rendered by default.

14 changes: 14 additions & 0 deletions CodingTracker/.vscode/launch.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
{
"version": "0.2.0",
"configurations": [
{
"name": "CodingTracker",
"type": "coreclr",
"request": "launch",
"preLaunchTask": "build",
"program": "${workspaceFolder}/bin/Debug/net10.0/CodingTracker.dll",
"cwd": "${workspaceFolder}",
"console": "integratedTerminal"
}
]
}
15 changes: 15 additions & 0 deletions CodingTracker/.vscode/tasks.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
{
"version": "2.0.0",
"tasks": [
{
"label": "build",
"command": "dotnet",
"type": "process",
"args": [
"build",
"${workspaceFolder}/CodingTracker.csproj"
],
"problemMatcher": "$msCompile"
}
]
}
18 changes: 18 additions & 0 deletions CodingTracker/CodingSession.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
namespace CodingTracker;

class CodingSession
{
public int Id {get; set;}
public DateTime StartTime {get; set;}
public DateTime EndTime {get; set;}
public int Duration {get; set;}

public string FormattedDuration
{
get
{
TimeSpan time = TimeSpan.FromMinutes(Duration);
return $"{(int)time.TotalHours:D2}:{time.Minutes:D2}";
}
}
}
21 changes: 21 additions & 0 deletions CodingTracker/CodingTracker.csproj
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
<Project Sdk="Microsoft.NET.Sdk">

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

<ItemGroup>
<PackageReference Include="Dapper" Version="2.1.89" />
<PackageReference Include="Microsoft.Data.Sqlite" Version="10.0.13" />
<PackageReference Include="Microsoft.Extensions.Configuration" Version="10.0.13" />
<PackageReference Include="Microsoft.Extensions.Configuration.Json" Version="10.0.13" />
<PackageReference Include="Spectre.Console" Version="0.57.2" />
<None Update="appsettings.json">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</None>
</ItemGroup>

</Project>
139 changes: 139 additions & 0 deletions CodingTracker/DataBase.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,139 @@
using System.Globalization;
using Dapper;
using Microsoft.Data.Sqlite;
using Microsoft.Extensions.Configuration;
using Spectre.Console;

namespace CodingTracker;

class DataBase
{
public static IConfiguration config = new ConfigurationBuilder()
.SetBasePath(Directory.GetCurrentDirectory())
.AddJsonFile("appsettings.json", optional: false, reloadOnChange: true)
.Build();

public static string? connectionString = config.GetConnectionString("DefaultConnection");

public static void CreateDataBase()
{
using (var connection = new SqliteConnection(connectionString))
{
connection.Open();
var tableCmd = connection.CreateCommand();

tableCmd.CommandText = @"CREATE TABLE IF NOT EXISTS coding_session (
Id INTEGER PRIMARY KEY AUTOINCREMENT,
StartTime TEXT,
EndTime TEXT,
Duration INTEGER
)";

tableCmd.ExecuteNonQuery();

connection.Close();
}
}

public static void GetAllRecords()
{
var sql = $"SELECT * FROM coding_session";
using var connection = new SqliteConnection(connectionString);
var codingSessions = connection.Query<CodingSession>(sql).ToList();
foreach (var session in codingSessions)
{
AnsiConsole.MarkupLine($"Session {session.Id} started at: {session.StartTime}, ended at: {session.EndTime}, duration: {session.FormattedDuration}");
}
}

public static void AddNewRecord()
{
string? StartTime = UserInput.GetTimeInput("Start time");
if (StartTime is null) return;

string? EndTime = UserInput.GetTimeInput("End time");
if (EndTime is null) return;

double Duration = DurationCalculator.CalculateDurationInMinutes(StartTime, EndTime);

using (var connection = new SqliteConnection(connectionString))
{
string sql = @"INSERT INTO coding_session (StartTime, EndTime, Duration) VALUES (@StartTime, @EndTime, @Duration);";

// Create the data object to insert
var newSession = new {StartTime, EndTime, Duration};
// Execute returns the number of rows inserted (should be 1)
int rowsAffected = connection.Execute(sql, newSession);

Console.WriteLine($"{rowsAffected} record added successfully.");
}
}

public static void UpdateRecord()
{
var id = AnsiConsole.Ask<int>("Enter Record [red]Id[/]");

string? StartTime = UserInput.GetTimeInput("Start time");
if (StartTime is null) return;

string? EndTime = UserInput.GetTimeInput("End time");
if (EndTime is null) return;

double Duration = DurationCalculator.CalculateDurationInMinutes(StartTime, EndTime);

using (var connection = new SqliteConnection(connectionString))
{
string sql = "UPDATE coding_session SET StartTime = @StartTime, EndTime = @EndTime, Duration = @Duration WHERE Id = @id;";

var updateSession = new {id, StartTime, EndTime, Duration};

int rowsAffected = connection.Execute(sql, updateSession);

Console.WriteLine($"{rowsAffected} record updated.");
}
}

public static void DeleteRecord()
{
var Id = AnsiConsole.Ask<int>("Enter Record [red]Id[/]");

using (var connection = new SqliteConnection(connectionString))
{
string sql = "DELETE FROM coding_session WHERE Id = @Id;";

int rowsAffected = connection.Execute(sql, new { Id });

if (rowsAffected > 0)
{
Console.WriteLine("Record deleted successfully.");
}
else
{
Console.WriteLine("No record found with that ID.");
}
}
}

}

public static class DurationCalculator
{
public static int CalculateDurationInMinutes(string startTimeStr, string endTimeStr)
{
// Parse the inputs (assuming today's date for simple calculations)
DateTime today = DateTime.Today;
DateTime startTime = DateTime.ParseExact($"{today:yyyy-MM-dd} {startTimeStr}", "yyyy-MM-dd HH:mm", CultureInfo.InvariantCulture);
DateTime endTime = DateTime.ParseExact($"{today:yyyy-MM-dd} {endTimeStr}", "yyyy-MM-dd HH:mm", CultureInfo.InvariantCulture);

// Midnight crossover check: if end time is before start time, it means next day
if (endTime < startTime)
{
endTime = endTime.AddDays(1);
}

TimeSpan difference = endTime - startTime;

// Return rounded total minutes as an integer
return (int)Math.Round(difference.TotalMinutes);
}
}
10 changes: 10 additions & 0 deletions CodingTracker/Program.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
namespace CodingTracker;

public class Program
{
static void Main(string[] args)
{
DataBase.CreateDataBase();
UserInput.GetUserInput();
}
}
66 changes: 66 additions & 0 deletions CodingTracker/UserInput.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
using System;
using System.Globalization;
using Spectre.Console;

namespace CodingTracker;

public class UserInput
{
public static void GetUserInput()
{
bool closeApp = false;
while (closeApp == false)
{
AnsiConsole.MarkupLine("\n[bold]Main Menu[/]");
AnsiConsole.MarkupLine("[grey]Type 0 to Close Application[/]");
AnsiConsole.MarkupLine("[grey]Type 1 to View All Records[/]");
AnsiConsole.MarkupLine("[grey]Type 2 to Add New Record[/]");
AnsiConsole.MarkupLine("[grey]Type 3 to Delete Record[/]");
AnsiConsole.MarkupLine("[grey]Type 4 to Update Record[/]");

string commandInput = AnsiConsole.Prompt(
new TextPrompt<string>("What would you like to do?")
.PromptStyle("cyan"));

switch (commandInput)
{
case "0":
AnsiConsole.MarkupLine("[green]Goodbye![/]");
closeApp = true;
break;
case "1":
DataBase.GetAllRecords();
break;
case "2":
DataBase.AddNewRecord();
break;
case "3":
DataBase.DeleteRecord();
break;
case "4":
DataBase.UpdateRecord();
break;
default:
AnsiConsole.MarkupLine("[red]Invalid input. Please try again.[/]");
break;
}
}
}

public static string? GetTimeInput(string timeType)
{
string prompt = $"Please enter the [red]{timeType}[/] time in 24-hour format HH:mm (or type 0 to return to the main menu):";
while (true)
{
string timeInput = AnsiConsole.Ask<string>(prompt);
if (timeInput == "0") return null;

if (DateTime.TryParseExact(timeInput, "HH:mm", new CultureInfo("en-US"), DateTimeStyles.None, out _))
{
return timeInput;
}

AnsiConsole.MarkupLine($"[red]Invalid {timeType} time. Use HH:mm, for example 14:30.[/]");
}
}
}
5 changes: 5 additions & 0 deletions CodingTracker/appsettings.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
{
"ConnectionStrings": {
"DefaultConnection": "Data Source= coding-tracker.db"
}
}
45 changes: 45 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
# Coding Tracker

A small .NET console application for tracking coding sessions. It uses Dapper for data access to a SQLite database, and Spectre.Console for the interactive menu.

## Features

- Add coding sessions with start and end times.
- View saved sessions and their durations.
- Update or delete a session by its ID.
- Calculate sessions that cross midnight.
- Cancel time entry by entering `0`.

## Requirements

- .NET 10 SDK

## Setup and Run

1. Clone this repository and open a terminal in the project directory.
2. In `appsettings.json`, set `ConnectionStrings:DefaultConnection` to a SQLite database location that exists on your machine. For example:

```json
"DefaultConnection": "Data Source=coding-tracker.db"
```

The checked-in configuration uses an absolute Windows path, so update it if that path does not exist on your machine.
3. Run the application:

```sh
dotnet run
```

Run it in an interactive terminal so Spectre.Console can accept keyboard input. In VS Code, press **F5** and choose the `CodingTracker` launch profile.

## Menu

| Option | Action |
| --- | --- |
| `0` | Close the application |
| `1` | View all records |
| `2` | Add a record |
| `3` | Delete a record by ID |
| `4` | Update a record by ID |

Enter start and end times in 24-hour `HH:mm` format, such as `14:30`. Enter `0` at either time prompt to cancel the add or update operation and return to the menu.
Loading