diff --git a/ExerciseTracker.Call911plz/.editorconfig b/ExerciseTracker.Call911plz/.editorconfig new file mode 100644 index 00000000..e224bd4b --- /dev/null +++ b/ExerciseTracker.Call911plz/.editorconfig @@ -0,0 +1,4 @@ +[*.cs] + +# CS8603: Possible null reference return. +dotnet_diagnostic.CS8603.severity = suggestion diff --git a/ExerciseTracker.Call911plz/Controller/ControllerBase.cs b/ExerciseTracker.Call911plz/Controller/ControllerBase.cs new file mode 100644 index 00000000..41ca2db7 --- /dev/null +++ b/ExerciseTracker.Call911plz/Controller/ControllerBase.cs @@ -0,0 +1,27 @@ +using Spectre.Console; + +public class ControllerBase +{ + internal virtual void OnStartOfLoop(){ Console.Clear(); } + internal virtual Task HandleUserInputAsync() { return Task.FromResult(false); } + + public async Task StartAsync() + { + bool exit = false; + + while (exit == false) + { + try { OnStartOfLoop(); } + catch (Exception e) { AnsiConsole.MarkupLine($"[bold red]Error on Start Of Loop[/]\n{e}"); } + + try { exit = await HandleUserInputAsync(); } + catch (Exception e) { AnsiConsole.MarkupLine($"[bold red]Error on Handling User Input[/]\n{e}"); } + + if (exit == false) + { + AnsiConsole.MarkupLine("[bold yellow]Press Enter to continue[/]"); + Console.Read(); + } + } + } +} \ No newline at end of file diff --git a/ExerciseTracker.Call911plz/Controller/ExerciseController.cs b/ExerciseTracker.Call911plz/Controller/ExerciseController.cs new file mode 100644 index 00000000..bd05607d --- /dev/null +++ b/ExerciseTracker.Call911plz/Controller/ExerciseController.cs @@ -0,0 +1,96 @@ +using Spectre.Console; + +public class ExerciseController(IService service) : ControllerBase +{ + IService _service = service; + + internal override void OnStartOfLoop() + { + Console.Clear(); + AnsiConsole.Write + ( + new FigletText("Exercise Tracker") + .LeftJustified() + .Color(Color.Red) + ); + } + + internal override async Task HandleUserInputAsync() + { + MenuEnums.Main input = GetMenu.MainMenu(); + + switch (input) + { + case MenuEnums.Main.CREATE: + await CreateAsync(); + break; + case MenuEnums.Main.READ: + await ReadByIdAsync(); + break; + case MenuEnums.Main.READALL: + ReadAll(); + break; + case MenuEnums.Main.UPDATE: + await UpdateAsync(); + break; + case MenuEnums.Main.DELETE: + await DeleteAsync(); + break; + case MenuEnums.Main.EXIT: + return true; + } + return false; + } + + private async Task CreateAsync() + { + Exercise exercise = GetData.GetExercise(); + Exercise createdExercise = await _service.AddAsync(exercise); + + AnsiConsole.MarkupLine($"[bold grey]Inserted new[/] [bold yellow]Exercise[/] [bold grey]to db:[/]"); + DisplayData.DisplayExercise([createdExercise]); + } + + private async Task ReadByIdAsync() + { + int id = GetData.GetId(); + Exercise exercise = await _service.GetByIdAsync(id) + ?? throw new Exception("[bold red]Could not find exercise with id[/]"); + + DisplayData.DisplayExercise([exercise]); + } + + private void ReadAll() + { + List exercises = _service.GetAll() ?? []; + + DisplayData.DisplayExercise(exercises); + } + + private async Task UpdateAsync() + { + List exercises = _service.GetAll() + ?? throw new Exception("No exercises to update"); + Exercise exerciseToUpdate = GetData.GetExerciseFromList(exercises); + + AnsiConsole.MarkupLine("[bold grey]Original: [/]"); + DisplayData.DisplayExercise([exerciseToUpdate]); + + Exercise updatedExercise = GetData.UpdateExercise(exerciseToUpdate); + Exercise updatedExerciseInDb = await _service.UpdateAsync(updatedExercise); + + AnsiConsole.MarkupLine("[bold grey]Updated: [/]"); + DisplayData.DisplayExercise([updatedExerciseInDb]); + } + + private async Task DeleteAsync() + { + List exercises = _service.GetAll() + ?? throw new Exception("No exercises to delete"); + + Exercise exerciseToDelete = GetData.GetExerciseFromList(exercises); + await _service.DeleteAsync(exerciseToDelete); + + AnsiConsole.MarkupLine("[bold grey]Exercise[/] [bold red]deleted[/]"); + } +} \ No newline at end of file diff --git a/ExerciseTracker.Call911plz/Data/Exercise.cs b/ExerciseTracker.Call911plz/Data/Exercise.cs new file mode 100644 index 00000000..00947c92 --- /dev/null +++ b/ExerciseTracker.Call911plz/Data/Exercise.cs @@ -0,0 +1,8 @@ +public class Exercise +{ + public int Id { get; set; } + public DateTime Start { get; set; } + public DateTime End { get; set; } + public TimeSpan Duration { get; set; } + public string Comments { get; set; } = string.Empty; +} \ No newline at end of file diff --git a/ExerciseTracker.Call911plz/Data/ExerciseContext.cs b/ExerciseTracker.Call911plz/Data/ExerciseContext.cs new file mode 100644 index 00000000..41241032 --- /dev/null +++ b/ExerciseTracker.Call911plz/Data/ExerciseContext.cs @@ -0,0 +1,17 @@ +using Microsoft.EntityFrameworkCore; + +public class ExerciseContext : DbContext +{ + public DbSet Exercises { get; set; } + + protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder) + { + optionsBuilder.UseSqlServer(@" + Server=localhost; + Database=exercisetrackerdb; + User Id=sa; + Password=StrongP@ssword1; + TrustServerCertificate=True + "); + } +} \ No newline at end of file diff --git a/ExerciseTracker.Call911plz/Data/Migrations/20250428201051_InitialCreate.Designer.cs b/ExerciseTracker.Call911plz/Data/Migrations/20250428201051_InitialCreate.Designer.cs new file mode 100644 index 00000000..fb0fd77c --- /dev/null +++ b/ExerciseTracker.Call911plz/Data/Migrations/20250428201051_InitialCreate.Designer.cs @@ -0,0 +1,55 @@ +// +using System; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Metadata; +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; + +#nullable disable + +namespace ExerciseTracker.Call911plz.Data.Migrations +{ + [DbContext(typeof(ExerciseContext))] + [Migration("20250428201051_InitialCreate")] + partial class InitialCreate + { + /// + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasAnnotation("ProductVersion", "9.0.4") + .HasAnnotation("Relational:MaxIdentifierLength", 128); + + SqlServerModelBuilderExtensions.UseIdentityColumns(modelBuilder); + + modelBuilder.Entity("Exercise", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("Comments") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("Duration") + .HasColumnType("time"); + + b.Property("End") + .HasColumnType("datetime2"); + + b.Property("Start") + .HasColumnType("datetime2"); + + b.HasKey("Id"); + + b.ToTable("Exercises"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/ExerciseTracker.Call911plz/Data/Migrations/20250428201051_InitialCreate.cs b/ExerciseTracker.Call911plz/Data/Migrations/20250428201051_InitialCreate.cs new file mode 100644 index 00000000..fc6c3c81 --- /dev/null +++ b/ExerciseTracker.Call911plz/Data/Migrations/20250428201051_InitialCreate.cs @@ -0,0 +1,38 @@ +using System; +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace ExerciseTracker.Call911plz.Data.Migrations +{ + /// + public partial class InitialCreate : Migration + { + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.CreateTable( + name: "Exercises", + columns: table => new + { + Id = table.Column(type: "int", nullable: false) + .Annotation("SqlServer:Identity", "1, 1"), + Start = table.Column(type: "datetime2", nullable: false), + End = table.Column(type: "datetime2", nullable: false), + Duration = table.Column(type: "time", nullable: false), + Comments = table.Column(type: "nvarchar(max)", nullable: false) + }, + constraints: table => + { + table.PrimaryKey("PK_Exercises", x => x.Id); + }); + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropTable( + name: "Exercises"); + } + } +} diff --git a/ExerciseTracker.Call911plz/Data/Migrations/ExerciseContextModelSnapshot.cs b/ExerciseTracker.Call911plz/Data/Migrations/ExerciseContextModelSnapshot.cs new file mode 100644 index 00000000..0a85374c --- /dev/null +++ b/ExerciseTracker.Call911plz/Data/Migrations/ExerciseContextModelSnapshot.cs @@ -0,0 +1,52 @@ +// +using System; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Metadata; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; + +#nullable disable + +namespace ExerciseTracker.Call911plz.Data.Migrations +{ + [DbContext(typeof(ExerciseContext))] + partial class ExerciseContextModelSnapshot : ModelSnapshot + { + protected override void BuildModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasAnnotation("ProductVersion", "9.0.4") + .HasAnnotation("Relational:MaxIdentifierLength", 128); + + SqlServerModelBuilderExtensions.UseIdentityColumns(modelBuilder); + + modelBuilder.Entity("Exercise", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("Comments") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("Duration") + .HasColumnType("time"); + + b.Property("End") + .HasColumnType("datetime2"); + + b.Property("Start") + .HasColumnType("datetime2"); + + b.HasKey("Id"); + + b.ToTable("Exercises"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/ExerciseTracker.Call911plz/Enums.cs b/ExerciseTracker.Call911plz/Enums.cs new file mode 100644 index 00000000..3635c1e2 --- /dev/null +++ b/ExerciseTracker.Call911plz/Enums.cs @@ -0,0 +1,4 @@ +public class MenuEnums +{ + public enum Main { CREATE, READ, READALL, UPDATE, DELETE, EXIT } +} \ No newline at end of file diff --git a/ExerciseTracker.Call911plz/ExerciseTracker.Call911plz.csproj b/ExerciseTracker.Call911plz/ExerciseTracker.Call911plz.csproj new file mode 100644 index 00000000..492ed385 --- /dev/null +++ b/ExerciseTracker.Call911plz/ExerciseTracker.Call911plz.csproj @@ -0,0 +1,20 @@ + + + + Exe + net9.0 + enable + enable + + + + + + runtime; build; native; contentfiles; analyzers; buildtransitive + all + + + + + + diff --git a/ExerciseTracker.Call911plz/ExerciseTracker.Call911plz.sln b/ExerciseTracker.Call911plz/ExerciseTracker.Call911plz.sln new file mode 100644 index 00000000..36ca3ddb --- /dev/null +++ b/ExerciseTracker.Call911plz/ExerciseTracker.Call911plz.sln @@ -0,0 +1,34 @@ + +Microsoft Visual Studio Solution File, Format Version 12.00 +# Visual Studio Version 17 +VisualStudioVersion = 17.0.31903.59 +MinimumVisualStudioVersion = 10.0.40219.1 +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "ExerciseTracker.Call911plz", "ExerciseTracker.Call911plz.csproj", "{2EC88879-AAFF-4268-8602-393DD1E114AB}" +EndProject +Global + GlobalSection(SolutionConfigurationPlatforms) = preSolution + Debug|Any CPU = Debug|Any CPU + Debug|x64 = Debug|x64 + Debug|x86 = Debug|x86 + Release|Any CPU = Release|Any CPU + Release|x64 = Release|x64 + Release|x86 = Release|x86 + EndGlobalSection + GlobalSection(ProjectConfigurationPlatforms) = postSolution + {2EC88879-AAFF-4268-8602-393DD1E114AB}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {2EC88879-AAFF-4268-8602-393DD1E114AB}.Debug|Any CPU.Build.0 = Debug|Any CPU + {2EC88879-AAFF-4268-8602-393DD1E114AB}.Debug|x64.ActiveCfg = Debug|Any CPU + {2EC88879-AAFF-4268-8602-393DD1E114AB}.Debug|x64.Build.0 = Debug|Any CPU + {2EC88879-AAFF-4268-8602-393DD1E114AB}.Debug|x86.ActiveCfg = Debug|Any CPU + {2EC88879-AAFF-4268-8602-393DD1E114AB}.Debug|x86.Build.0 = Debug|Any CPU + {2EC88879-AAFF-4268-8602-393DD1E114AB}.Release|Any CPU.ActiveCfg = Release|Any CPU + {2EC88879-AAFF-4268-8602-393DD1E114AB}.Release|Any CPU.Build.0 = Release|Any CPU + {2EC88879-AAFF-4268-8602-393DD1E114AB}.Release|x64.ActiveCfg = Release|Any CPU + {2EC88879-AAFF-4268-8602-393DD1E114AB}.Release|x64.Build.0 = Release|Any CPU + {2EC88879-AAFF-4268-8602-393DD1E114AB}.Release|x86.ActiveCfg = Release|Any CPU + {2EC88879-AAFF-4268-8602-393DD1E114AB}.Release|x86.Build.0 = Release|Any CPU + EndGlobalSection + GlobalSection(SolutionProperties) = preSolution + HideSolutionNode = FALSE + EndGlobalSection +EndGlobal diff --git a/ExerciseTracker.Call911plz/Model/ExerciseRepository.cs b/ExerciseTracker.Call911plz/Model/ExerciseRepository.cs new file mode 100644 index 00000000..9cd9a0ff --- /dev/null +++ b/ExerciseTracker.Call911plz/Model/ExerciseRepository.cs @@ -0,0 +1,54 @@ +public interface IRepository +{ + public Task AddAsync(Exercise log); + public List? GetAll(); + public Task GetByIdAsync(int id); + public Task UpdateAsync(Exercise log); + public Task DeleteAsync(Exercise log); +} + +public class ExerciseRepository : IRepository +{ + private readonly ExerciseContext _exerciseContext; + + public ExerciseRepository(ExerciseContext exerciseContext) + { + _exerciseContext = exerciseContext; + } + + public async Task AddAsync(Exercise log) + { + var savedResult = await _exerciseContext.Exercises.AddAsync(log); + await _exerciseContext.SaveChangesAsync(); + return savedResult.Entity; + } + + public List? GetAll() + { + return _exerciseContext.Exercises.ToList(); + } + + public async Task GetByIdAsync(int id) + { + Exercise? result = await _exerciseContext.Exercises.FindAsync(id); + return result; + } + + public async Task UpdateAsync(Exercise log) + { + Exercise logInDb = await _exerciseContext.Exercises.FindAsync(log.Id) + ?? throw new Exception("Could not find log to update"); + + _exerciseContext.Entry(logInDb).CurrentValues.SetValues(log); + + await _exerciseContext.SaveChangesAsync(); + return _exerciseContext.Entry(logInDb).Entity; + } + + public async Task DeleteAsync(Exercise log) + { + var savedResult = _exerciseContext.Exercises.Remove(log); + await _exerciseContext.SaveChangesAsync(); + return savedResult.Entity; + } +} \ No newline at end of file diff --git a/ExerciseTracker.Call911plz/Model/ExerciseService.cs b/ExerciseTracker.Call911plz/Model/ExerciseService.cs new file mode 100644 index 00000000..f21f6ce6 --- /dev/null +++ b/ExerciseTracker.Call911plz/Model/ExerciseService.cs @@ -0,0 +1,39 @@ +public interface IService +{ + public Task AddAsync(Exercise log); + public List? GetAll(); + public Task GetByIdAsync(int id); + public Task UpdateAsync(Exercise log); + public Task DeleteAsync(Exercise log); +} + +public class ExerciseService(IRepository repo) : IService +{ + public async Task AddAsync(Exercise log) + { + Exercise exerciseAdded = await repo.AddAsync(log); + return exerciseAdded; + } + + public List? GetAll() + { + return repo.GetAll(); + } + + public async Task GetByIdAsync(int id) + { + return await repo.GetByIdAsync(id); + } + + public async Task UpdateAsync(Exercise log) + { + Exercise updatedExercise = await repo.UpdateAsync(log); + return updatedExercise; + } + + public async Task DeleteAsync(Exercise log) + { + Exercise deletedExercise = await repo.DeleteAsync(log); + return deletedExercise; + } +} diff --git a/ExerciseTracker.Call911plz/Program.cs b/ExerciseTracker.Call911plz/Program.cs new file mode 100644 index 00000000..4c6176ec --- /dev/null +++ b/ExerciseTracker.Call911plz/Program.cs @@ -0,0 +1,14 @@ +namespace ExerciseTracker.Call911plz; + +class Program +{ + static async Task Main(string[] args) + { + ExerciseContext context = new(); + ExerciseRepository exerciseRepository = new(context); + ExerciseService exerciseService = new(exerciseRepository); + ExerciseController exerciseController = new(exerciseService); + + await exerciseController.StartAsync(); + } +} diff --git a/ExerciseTracker.Call911plz/View/DisplayData.cs b/ExerciseTracker.Call911plz/View/DisplayData.cs new file mode 100644 index 00000000..63643a93 --- /dev/null +++ b/ExerciseTracker.Call911plz/View/DisplayData.cs @@ -0,0 +1,21 @@ +using Spectre.Console; + +public static class DisplayData +{ + public static void DisplayExercise(List exercises) + { + Table table = new(); + table.AddColumns(["Id", "Start Date", "End Date", "Duration", "Additional Comments"]); + + foreach(Exercise exercise in exercises) + table.AddRow([ + exercise.Id.ToString(), + exercise.Start.ToString(), + exercise.End.ToString(), + exercise.Duration.ToString(), + exercise.Comments.ToString(), + ]); + + AnsiConsole.Write(table); + } +} \ No newline at end of file diff --git a/ExerciseTracker.Call911plz/View/UserInput.cs b/ExerciseTracker.Call911plz/View/UserInput.cs new file mode 100644 index 00000000..39295a40 --- /dev/null +++ b/ExerciseTracker.Call911plz/View/UserInput.cs @@ -0,0 +1,101 @@ +using Spectre.Console; + +public static class GetData +{ + public static int GetId() + { + TextPrompt prompt = new("[bold grey]Enter id[/]"); + return AnsiConsole.Prompt(prompt); + } + + public static Exercise GetExerciseFromList(List exercises) + { + SelectionPrompt prompt = new(); + prompt.Title("[bold grey]Select from below[/]"); + prompt.AddChoices(exercises); + prompt.UseConverter((selection) => { + return $"{selection.Id} {selection.Start} {selection.End}"; + }); + + return AnsiConsole.Prompt(prompt); + } + + public static Exercise GetExercise() + { + Exercise exercise = new() + { + Id = default, + Start = GetDateTime("Enter start time: "), + End = GetDateTime("Enter end time: "), + Comments = GetComments(), + }; + exercise.Duration = exercise.End - exercise.Start; + + return exercise; + } + + public static Exercise UpdateExercise(Exercise existingExercise) + { + Exercise exercise = new() + { + Id = existingExercise.Id, + Start = GetDateTime("Enter updated start time: ", existingExercise.Start), + End = GetDateTime("Enter updated end time: ", existingExercise.End), + Comments = GetComments(existingExercise.Comments), + }; + exercise.Duration = exercise.End - exercise.Start; + + return exercise; + } + + private static DateTime GetDateTime(string stringPrompt, DateTime existingDateTime = default) + { + TextPrompt prompt = new($"[bold grey]{stringPrompt}[/]"); + + if (existingDateTime != default) + prompt.DefaultValue(existingDateTime.ToString()); + else + prompt.DefaultValue(DateTime.Now.ToString()); + + prompt.Validate( (input) => { + if (DateTime.TryParse(input, out var _)) + return ValidationResult.Success(); + return ValidationResult.Error("Invalid date time format"); + }); + + return DateTime.Parse(AnsiConsole.Prompt(prompt)); + } + + private static string GetComments(string? existingComments = default) + { + TextPrompt prompt = new("[bold grey](Optional) Additional comments[/]"); + + prompt.AllowEmpty(); + + if (existingComments != default) + prompt.DefaultValue(existingComments); + + return AnsiConsole.Prompt(prompt); + } +} + +public static class GetMenu +{ + public static MenuEnums.Main MainMenu() + { + return AnsiConsole.Prompt( + new SelectionPrompt() + .AddChoices(Enum.GetValues()) + .UseConverter((input) => input switch + { + MenuEnums.Main.CREATE => "Create new exercise log", + MenuEnums.Main.READ => "Find specific exercise log", + MenuEnums.Main.READALL => "Find all exercise logs", + MenuEnums.Main.UPDATE => "Update exercise log", + MenuEnums.Main.DELETE => "Delete exercise log", + MenuEnums.Main.EXIT => "Exit program", + _ => throw new Exception("Selection somehow went wrong") + }) + ); + } +} \ No newline at end of file