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
11 changes: 11 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
Project: Tracking coding time.

Requirements/Comments/Thoughts:
Use Dapper instead of ADO.NET for the SQLite side of things.
Use Spectre Console for displaying the information. This was really good as it allowed me to display the records in a proper table and looked much nicer. Also having the ability to allow interactive user selections is nice. It also had the added benefit of ensuring the option that was being returned to the code - removing the risk of the user typing anything they wanted into it.
Validations on dates, selections, end time must be after start time.
Use separate classes - this was good as it kept everything organised. It also made me think of where to put certain methods as some of them could have been in a couple of different classes.
Initially did the date format the same the previous one (dd-MM-yyyy HH:mm) but then changed this to match the format that SQLite reads dates (ISO 8601). I chose the date I'm familiar with from the SQL I used in work (yyyy-MM-dd HH:mm).
Created the config file - I found this a bit tricky to find out how to actually read the fields from the json file. Struggled to find information online about this but did find it eventually!
Coding session is used when reading the records from the table.
Duration is calculated by the code.
3 changes: 3 additions & 0 deletions codingTracker.stevenwardlaw/codingTracker.stevenwardlaw.slnx
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
<Solution>
<Project Path="codingTracker.stevenwardlaw/codingTracker.stevenwardlaw.csproj" />
</Solution>
Original file line number Diff line number Diff line change
@@ -0,0 +1,111 @@
using Dapper;
using Microsoft.Data.Sqlite;
using Microsoft.Extensions.Configuration;
using Spectre.Console;
using static System.Collections.Specialized.BitVector32;


namespace codingTracker.stevenwardlaw
{
internal static class CodingController
{

static string CreateConnectionString()
{
IConfiguration configuration = new ConfigurationBuilder().AddJsonFile("appsettings.json").Build();
string connectionString = configuration.GetConnectionString("DefaultConnection")!;

return connectionString;
}

public static void CreateTable()
{
using (var connection = new SqliteConnection(CreateConnectionString()))
{
connection.Open();
var tableCmd = connection.CreateCommand();
tableCmd.CommandText = @"CREATE TABLE IF NOT EXISTS codingTracker (
Id INTEGER PRIMARY KEY AUTOINCREMENT,
StartTime TEXT,
EndTime TEXT,
Duration INTEGER
)";
tableCmd.ExecuteNonQuery();
connection.Close();
}
}

public static int InsertRecord(string _startTime, string _endTime)
{
using (var connection = new SqliteConnection(CreateConnectionString()))
{
connection.Open();
string sql = "INSERT INTO codingTracker (StartTime, EndTime, Duration)" +
"VALUES (@starttime, @endtime, @duration)";
// Create object to hold the parameters and assign these from the codingSession instance
object[] parameters = { new { starttime = _startTime, endtime = _endTime,
duration = GetDuration(_startTime, _endTime)} };
int numRecords = connection.Execute(sql, parameters);
connection.Close();
return numRecords;
}
}

public static int UpdateRecord(int _id, string _startTime, string _endTime)
{
using (var connection = new SqliteConnection(CreateConnectionString()))
{
connection.Open();
string sql = "UPDATE codingTracker SET StartTime = @starttime, EndTime = @endtime," +
"Duration = @duration WHERE Id = @id";
object[] parameters = { new { id = _id, starttime = _startTime,
endtime = _endTime, duration = GetDuration(_startTime, _endTime)} };
int numRecords = connection.Execute(sql, parameters);
connection.Close();
return numRecords;
}
}

public static int DeleteRecord(int _id)
{
using (var connection = new SqliteConnection(CreateConnectionString()))
{
connection.Open();
string sql = "DELETE FROM codingTracker WHERE Id = @id";
int numRecords = connection.Execute(sql, new { id = _id });
connection.Close();
return numRecords;
}
}

public static void GetAllRecords()
{
using (var connection = new SqliteConnection(CreateConnectionString()))
{
connection.Open();
string sql = UserInput.ViewRecordOptions();
List<CodingSession> sessions = connection.Query<CodingSession>(sql).ToList();

var table = new Table();
table.AddColumns("Id", "Start Time", "End Time", "Duration (minutes)");

foreach (CodingSession session in sessions)
{
table.AddRow(session.Id.ToString(), session.StartTime, session.EndTime, session.Duration);
}

AnsiConsole.Write(table);
connection.Close();
}
}

private static int GetDuration(string _startTime, string _endTime)
{
DateTime startTime = DateTime.ParseExact(_startTime, "yyyy-MM-dd HH:mm", null);
DateTime endTime = DateTime.ParseExact(_endTime, "yyyy-MM-dd HH:mm", null);
TimeSpan duration = endTime - startTime;
int durationMinutes = (int)duration.TotalMinutes;
return durationMinutes;
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
namespace codingTracker.stevenwardlaw
{
internal class CodingSession
{
public int Id { get; set; }
public string StartTime { get; set; }
public string EndTime { get; set; }
public string Duration { get; set; }
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
namespace codingTracker.stevenwardlaw
{
internal static class DataValidation
{
public static bool ValidateDate(string date)
{
return DateTime.TryParseExact(date, "yyyy-MM-dd HH:mm", null, 0, out DateTime result);
}

public static bool ValidateNumber(string num)
{
return Int16.TryParse(num, out short result);
}

public static bool IsEndDateAfter(string _startTime, string _endTime)
{
DateTime startTime = DateTime.ParseExact(_startTime, "yyyy-MM-dd HH:mm", null);
DateTime endTime = DateTime.ParseExact(_endTime, "yyyy-MM-dd HH:mm", null);
if (endTime > startTime) return true;
else return false;
}
}
}
19 changes: 19 additions & 0 deletions codingTracker.stevenwardlaw/codingTracker.stevenwardlaw/Program.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
namespace codingTracker.stevenwardlaw
{
internal class Program
{
static void Main(string[] args)
{
bool appState = true;

CodingController.CreateTable();

while (appState)
{
UserInput.DisplayOptions();
}

}

}
}
158 changes: 158 additions & 0 deletions codingTracker.stevenwardlaw/codingTracker.stevenwardlaw/UserInput.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,158 @@
using Spectre.Console;

namespace codingTracker.stevenwardlaw
{
internal static class UserInput
{
public static void DisplayOptions()
{
string option = AnsiConsole.Prompt(
new SelectionPrompt<string>()
.Title("What would you like to do?")
.AddChoices("View all records", "Add a record", "Update a record", "Delete a record"));

switch (option)
{
case "View all records":
AnsiConsole.Clear();
CodingController.GetAllRecords();
break;
case "Add a record":
AnsiConsole.Clear();
int addedRows = UserInsertRecord();
if (addedRows > 0) AnsiConsole.MarkupLine("The record was [green]successfully added.[/]");
else AnsiConsole.MarkupLine("[red]Warning:[/] No records were added.");
break;
case "Update a record":
AnsiConsole.Clear();
CodingController.GetAllRecords();
int updatedRows = UserUpdateRecord();
if (updatedRows > 0) AnsiConsole.MarkupLine("The record was [green]successfully updated.[/]");
else AnsiConsole.MarkupLine("[red]Warning:[/] No records were updated.");
break;
case "Delete a record":
AnsiConsole.Clear();
CodingController.GetAllRecords();
int deletedRows = CodingController.DeleteRecord(
GetIdFromUser("Please enter the ID of the record you want to delete: "));
if (deletedRows > 0) AnsiConsole.MarkupLine("The record was [green]successfully deleted.[/]");
else AnsiConsole.MarkupLine("[red]Warning:[/] No records were deleted.");
break;
}
}

private static string GetDateFromUser(string message)
{
string date = "";
date = AnsiConsole.Ask<string>($"[bold]{message}[/]");
while (!DataValidation.ValidateDate(date))
{
date = AnsiConsole.Ask<string>("[bold]That is not in the correct date format, please enter a correct date: [/]");
}

return date;
}

private static int GetIdFromUser(string message)
{
string input = AnsiConsole.Ask<string>($"[bold]{message}[/]");
while (!DataValidation.ValidateNumber(input))
{
input = AnsiConsole.Ask<string>("[bold]That is not a valid number, please enter an ID number: [/]");
}
return Convert.ToInt16(input);
}

private static int UserUpdateRecord()
{
int id = GetIdFromUser("Please enter the ID of the record you want to update: ");
string startTime = GetDateFromUser("Please enter the new start date and time (in the following format yyyy-mm-dd hh:mm e.g. 2025-12-30 16:05): ");
string endTime = GetDateFromUser("Please enter the new end date and time (in the following format yyyy-mm-dd hh:mm e.g. 2025-12-30 16:05): ");

while (!DataValidation.IsEndDateAfter(startTime, endTime))
{
AnsiConsole.MarkupLine("[red]Error:[/] The end time must be [bold]after[/] the start time.");
startTime = GetDateFromUser("Please enter the new start date and time (in the following format yyyy-mm-dd hh:mm e.g. 2025-12-30 16:05): ");
endTime = GetDateFromUser("Please enter the new end date and time (in the following format yyyy-mm-dd hh:mm e.g. 2025-12-30 16:05): ");
}
int num = CodingController.UpdateRecord(id, startTime, endTime);
return num;
}

private static int UserInsertRecord()
{
string startTime = GetDateFromUser("Please enter the start date and time (in the following format yyyy-mm-dd hh:mm e.g. 2025-12-30 16:05): ");
string endTime = GetDateFromUser("Please enter the end date and time (in the following format yyyy-mm-dd hh:mm e.g. 2025-12-30 16:05): ");

while (!DataValidation.IsEndDateAfter(startTime, endTime))
{
AnsiConsole.MarkupLine("[red]Error:[/] The end time must be [bold]after[/] the start time.");
startTime = GetDateFromUser("Please enter the start date and time (in the following format yyyy-mm-dd hh:mm e.g. 2025-12-30 16:05): ");
endTime = GetDateFromUser("Please enter the end date and time (in the following format yyyy-mm-dd hh:mm e.g. 2025-12-30 16:05): ");
}
int num = CodingController.InsertRecord(startTime, endTime);
return num;
}

public static string ViewRecordOptions()
{
string dateFilter;
string sortOrder;

// Get option for filtering
string dateFilterOption = AnsiConsole.Prompt(
new SelectionPrompt<string>()
.Title("Would you like to filter for records that started in a specific time period?")
.AddChoices("No, all records", "Past day", "Past week", "Past month", "Past year"));

switch (dateFilterOption)
{
case "Past day":
dateFilter = "WHERE starttime > datetime('now','-1 days') ";
break;
case "Past week":
dateFilter = "WHERE starttime > datetime('now','-7 days') ";
break;
case "Past month":
dateFilter = "WHERE starttime > datetime('now','-1 month') ";
break;
case "Past year":
dateFilter = "WHERE starttime > datetime('now','-1 year') ";
break;
default:
dateFilter = "";
break;
}

// Get option for sorting
string sortFilterOption = AnsiConsole.Prompt(
new SelectionPrompt<string>()
.Title("Would you like to sort the records based on the Start Time or End Time?")
.AddChoices("No", "Start Time (ascending)", "Start Time (descending)",
"End Time (ascending)", "End Time (descending)"));

switch (sortFilterOption)
{
case "Start Time (ascending)":
sortOrder = "ORDER BY starttime ASC";
break;
case "Start Time (descending)":
sortOrder = "ORDER BY starttime DESC";
break;
case "End Time (ascending)":
sortOrder = "ORDER BY endtime ASC";
break;
case "End Time (descending)":
sortOrder = "ORDER BY endtime DESC";
break;
default:
sortOrder = "";
break;
}

string fullSql = "SELECT * from codingTracker " + dateFilter + sortOrder;
return fullSql;

}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
{
"ConnectionStrings": {
"DefaultConnection": "Data Source=codingTracker.db"
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
<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.72" />
<PackageReference Include="Microsoft.Data.Sqlite" Version="10.0.5" />
<PackageReference Include="Microsoft.Extensions.Configuration" Version="10.0.5" />
<PackageReference Include="Microsoft.Extensions.Configuration.Json" Version="10.0.5" />
<PackageReference Include="Newtonsoft.Json" Version="13.0.4" />
<PackageReference Include="Spectre.Console" Version="0.54.0" />
</ItemGroup>

<ItemGroup>
<None Update="appsettings.json">
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
</None>
</ItemGroup>

</Project>