From e8280d262271b54bff199a26425f36a5de19ff54 Mon Sep 17 00:00:00 2001 From: "pientka.k" Date: Sun, 27 Mar 2022 13:21:54 +0200 Subject: [PATCH 01/21] added migrations --- ...220327080819_AddCustomersTable.Designer.cs | 83 +++++++++++++++++++ .../20220327080819_AddCustomersTable.cs | 37 +++++++++ .../ApplicationDbContextModelSnapshot.cs | 27 ++++++ 3 files changed, 147 insertions(+) create mode 100644 DeveloperTest/Migrations/20220327080819_AddCustomersTable.Designer.cs create mode 100644 DeveloperTest/Migrations/20220327080819_AddCustomersTable.cs diff --git a/DeveloperTest/Migrations/20220327080819_AddCustomersTable.Designer.cs b/DeveloperTest/Migrations/20220327080819_AddCustomersTable.Designer.cs new file mode 100644 index 0000000..9b90d44 --- /dev/null +++ b/DeveloperTest/Migrations/20220327080819_AddCustomersTable.Designer.cs @@ -0,0 +1,83 @@ +// +using System; +using DeveloperTest.Database; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Metadata; +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; + +#nullable disable + +namespace DeveloperTest.Migrations +{ + [DbContext(typeof(ApplicationDbContext))] + [Migration("20220327080819_AddCustomersTable")] + partial class AddCustomersTable + { + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasAnnotation("ProductVersion", "6.0.1") + .HasAnnotation("Relational:MaxIdentifierLength", 128); + + SqlServerModelBuilderExtensions.UseIdentityColumns(modelBuilder, 1L, 1); + + modelBuilder.Entity("DeveloperTest.Database.Models.Customer", b => + { + b.Property("CustomerId") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("CustomerId"), 1L, 1); + + b.Property("Name") + .HasColumnType("nvarchar(max)"); + + b.Property("Type") + .HasColumnType("int"); + + b.HasKey("CustomerId"); + + b.ToTable("Customers"); + + b.HasData( + new + { + CustomerId = 1, + Name = "TestCustomer", + Type = 0 + }); + }); + + modelBuilder.Entity("DeveloperTest.Database.Models.Job", b => + { + b.Property("JobId") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("JobId"), 1L, 1); + + b.Property("Engineer") + .HasColumnType("nvarchar(max)"); + + b.Property("When") + .HasColumnType("datetime2"); + + b.HasKey("JobId"); + + b.ToTable("Jobs"); + + b.HasData( + new + { + JobId = 1, + Engineer = "Test", + When = new DateTime(2022, 2, 1, 12, 0, 0, 0, DateTimeKind.Unspecified) + }); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/DeveloperTest/Migrations/20220327080819_AddCustomersTable.cs b/DeveloperTest/Migrations/20220327080819_AddCustomersTable.cs new file mode 100644 index 0000000..82fa7d3 --- /dev/null +++ b/DeveloperTest/Migrations/20220327080819_AddCustomersTable.cs @@ -0,0 +1,37 @@ +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace DeveloperTest.Migrations +{ + public partial class AddCustomersTable : Migration + { + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.CreateTable( + name: "Customers", + columns: table => new + { + CustomerId = table.Column(type: "int", nullable: false) + .Annotation("SqlServer:Identity", "1, 1"), + Name = table.Column(type: "nvarchar(max)", nullable: true), + Type = table.Column(type: "int", nullable: false) + }, + constraints: table => + { + table.PrimaryKey("PK_Customers", x => x.CustomerId); + }); + + migrationBuilder.InsertData( + table: "Customers", + columns: new[] { "CustomerId", "Name", "Type" }, + values: new object[] { 1, "TestCustomer", 0 }); + } + + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropTable( + name: "Customers"); + } + } +} diff --git a/DeveloperTest/Migrations/ApplicationDbContextModelSnapshot.cs b/DeveloperTest/Migrations/ApplicationDbContextModelSnapshot.cs index 0ee623b..4ec2dd0 100644 --- a/DeveloperTest/Migrations/ApplicationDbContextModelSnapshot.cs +++ b/DeveloperTest/Migrations/ApplicationDbContextModelSnapshot.cs @@ -22,6 +22,33 @@ protected override void BuildModel(ModelBuilder modelBuilder) SqlServerModelBuilderExtensions.UseIdentityColumns(modelBuilder, 1L, 1); + modelBuilder.Entity("DeveloperTest.Database.Models.Customer", b => + { + b.Property("CustomerId") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("CustomerId"), 1L, 1); + + b.Property("Name") + .HasColumnType("nvarchar(max)"); + + b.Property("Type") + .HasColumnType("int"); + + b.HasKey("CustomerId"); + + b.ToTable("Customers"); + + b.HasData( + new + { + CustomerId = 1, + Name = "TestCustomer", + Type = 0 + }); + }); + modelBuilder.Entity("DeveloperTest.Database.Models.Job", b => { b.Property("JobId") From b74999c6feeafe4aafc7e1f562089839dda96b38 Mon Sep 17 00:00:00 2001 From: "pientka.k" Date: Sun, 27 Mar 2022 13:22:37 +0200 Subject: [PATCH 02/21] added new db se representing customers and added running migrations during application startup --- .../Database/ApplicationDbContext.cs | 21 +++++++++++++++++-- DeveloperTest/Program.cs | 14 ++++++++++++- 2 files changed, 32 insertions(+), 3 deletions(-) diff --git a/DeveloperTest/Database/ApplicationDbContext.cs b/DeveloperTest/Database/ApplicationDbContext.cs index f5be4a1..61da997 100644 --- a/DeveloperTest/Database/ApplicationDbContext.cs +++ b/DeveloperTest/Database/ApplicationDbContext.cs @@ -7,6 +7,7 @@ namespace DeveloperTest.Database public class ApplicationDbContext : DbContext { public DbSet Jobs { get; set; } + public DbSet Customers { get; set; } public ApplicationDbContext(DbContextOptions options) : base(options) { @@ -15,8 +16,6 @@ public ApplicationDbContext(DbContextOptions options) : ba protected override void OnModelCreating(ModelBuilder modelBuilder) { - base.OnModelCreating(modelBuilder); - modelBuilder.Entity() .HasKey(x => x.JobId); @@ -31,6 +30,24 @@ protected override void OnModelCreating(ModelBuilder modelBuilder) Engineer = "Test", When = new DateTime(2022, 2, 1, 12, 0, 0) }); + + modelBuilder.Entity() + .HasKey(x => x.CustomerId); + + modelBuilder.Entity() + .Property(x => x.CustomerId) + .ValueGeneratedOnAdd(); + + modelBuilder.Entity() + .HasData(new Customer + { + CustomerId = 1, + Name = "TestCustomer", + Type = CustomerType.Small + }); + + base.OnModelCreating(modelBuilder); + } } } diff --git a/DeveloperTest/Program.cs b/DeveloperTest/Program.cs index 507adef..b4913ad 100644 --- a/DeveloperTest/Program.cs +++ b/DeveloperTest/Program.cs @@ -1,4 +1,7 @@ +using DeveloperTest.Database; using Microsoft.AspNetCore.Hosting; +using Microsoft.EntityFrameworkCore; +using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Hosting; namespace DeveloperTest @@ -7,7 +10,16 @@ public class Program { public static void Main(string[] args) { - CreateHostBuilder(args).Build().Run(); + var host = CreateHostBuilder(args).Build(); + + // run migrations during startup + using (var scope = host.Services.CreateScope()) + { + var db = scope.ServiceProvider.GetRequiredService(); + db.Database.Migrate(); + } + + host.Run(); } public static IHostBuilder CreateHostBuilder(string[] args) => From 623d074442e3c6164fa50bec4fb3028d1b951d39 Mon Sep 17 00:00:00 2001 From: "pientka.k" Date: Sun, 27 Mar 2022 13:23:37 +0200 Subject: [PATCH 03/21] added customer types, mappings, serivces and data access --- DeveloperTest/Business/CustomerService.cs | 45 ++++++++++++++++ .../Business/Interfaces/ICustomerService.cs | 12 +++++ .../Controllers/CustomerController.cs | 52 +++++++++++++++++++ DeveloperTest/Database/Models/Customer.cs | 15 ++++++ .../Mappers/CustomerMappingExtensions.cs | 37 +++++++++++++ DeveloperTest/Models/BaseCustomerModel.cs | 11 ++++ DeveloperTest/Models/CustomerModel.cs | 13 +++++ DeveloperTest/Startup.cs | 1 + 8 files changed, 186 insertions(+) create mode 100644 DeveloperTest/Business/CustomerService.cs create mode 100644 DeveloperTest/Business/Interfaces/ICustomerService.cs create mode 100644 DeveloperTest/Controllers/CustomerController.cs create mode 100644 DeveloperTest/Database/Models/Customer.cs create mode 100644 DeveloperTest/Mappers/CustomerMappingExtensions.cs create mode 100644 DeveloperTest/Models/BaseCustomerModel.cs create mode 100644 DeveloperTest/Models/CustomerModel.cs diff --git a/DeveloperTest/Business/CustomerService.cs b/DeveloperTest/Business/CustomerService.cs new file mode 100644 index 0000000..50888ac --- /dev/null +++ b/DeveloperTest/Business/CustomerService.cs @@ -0,0 +1,45 @@ +using DeveloperTest.Business.Interfaces; +using DeveloperTest.Database; +using DeveloperTest.Database.Models; +using DeveloperTest.Mappers; +using DeveloperTest.Models; +using System.Threading.Tasks; + +namespace DeveloperTest.Business +{ + public class CustomerService : ICustomerService + { + private readonly ApplicationDbContext context; + + public CustomerService(ApplicationDbContext context) + { + this.context = context; + } + + public async Task CreateCustomerAsync(BaseCustomerModel model) + { + var customer = context.Customers.Add(new Customer + { + Name = model.Name, + Type = model.Type + }); + + await context.SaveChangesAsync(); + + return customer + .Entity + .ToModel(); + } + + public async Task GetCustomerAsync(int id) + { + var customer = await context.Customers.FindAsync(id); + return customer?.ToModel(); + } + + public Task GetCustomersAsync() + { + throw new System.NotImplementedException(); + } + } +} diff --git a/DeveloperTest/Business/Interfaces/ICustomerService.cs b/DeveloperTest/Business/Interfaces/ICustomerService.cs new file mode 100644 index 0000000..75eb44b --- /dev/null +++ b/DeveloperTest/Business/Interfaces/ICustomerService.cs @@ -0,0 +1,12 @@ +using DeveloperTest.Models; +using System.Threading.Tasks; + +namespace DeveloperTest.Business.Interfaces +{ + public interface ICustomerService + { + public Task GetCustomersAsync(); + public Task GetCustomerAsync(int id); + public Task CreateCustomerAsync(BaseCustomerModel model); + } +} diff --git a/DeveloperTest/Controllers/CustomerController.cs b/DeveloperTest/Controllers/CustomerController.cs new file mode 100644 index 0000000..24c6ff1 --- /dev/null +++ b/DeveloperTest/Controllers/CustomerController.cs @@ -0,0 +1,52 @@ +using System; +using Microsoft.AspNetCore.Mvc; +using DeveloperTest.Business.Interfaces; +using DeveloperTest.Models; +using System.Threading.Tasks; + +namespace DeveloperTest.Controllers +{ + [ApiController, Route("[controller]")] + public class CustomerController : ControllerBase + { + private readonly ICustomerService customerService; + + public CustomerController(ICustomerService customerService) + { + this.customerService = customerService; + } + + [HttpGet] + public async ValueTask Get() + { + return Ok(await customerService.GetCustomersAsync()); + } + + [HttpGet("{id}")] + public async ValueTask Get(int id) + { + var customer = await customerService.GetCustomerAsync(id); + + if (customer == null) + { + return NotFound(); + } + + return Ok(customer); + } + + [HttpPost] + public async ValueTask Create(BaseCustomerModel model) + { + const int minCustomerNameLength = 5; + if (model.Name.Length < 5) + { + return BadRequest($"Customer name cannot have less than {minCustomerNameLength} characters"); + } + + var customer = await customerService.CreateCustomerAsync(model); + + return Created($"customer/{customer.CustomerId}", customer); + } + } +} \ No newline at end of file diff --git a/DeveloperTest/Database/Models/Customer.cs b/DeveloperTest/Database/Models/Customer.cs new file mode 100644 index 0000000..d6225f2 --- /dev/null +++ b/DeveloperTest/Database/Models/Customer.cs @@ -0,0 +1,15 @@ +namespace DeveloperTest.Database.Models +{ + public class Customer + { + public int CustomerId { get; set; } + public string Name { get; set; } + public CustomerType Type { get; set; } + } + + public enum CustomerType + { + Small, + Large + } +} diff --git a/DeveloperTest/Mappers/CustomerMappingExtensions.cs b/DeveloperTest/Mappers/CustomerMappingExtensions.cs new file mode 100644 index 0000000..3f92469 --- /dev/null +++ b/DeveloperTest/Mappers/CustomerMappingExtensions.cs @@ -0,0 +1,37 @@ +using DeveloperTest.Database.Models; +using DeveloperTest.Models; + +namespace DeveloperTest.Mappers +{ + public static class CustomerMappingExtensions + { + public static Customer ToCustomerDbEntity(this CustomerModel source) + { + return new Customer() + { + CustomerId = source.CustomerId, + Name = source.Name, + Type = source.Type, + }; + } + + public static BaseCustomerModel ToBaseCustomerModel(this CustomerModel source) + { + return new BaseCustomerModel() + { + Name = source.Name, + Type = source.Type + }; + } + + public static CustomerModel ToModel(this Customer source) + { + return new CustomerModel() + { + CustomerId = source.CustomerId, + Name = source.Name, + Type = source.Type + }; + } + } +} diff --git a/DeveloperTest/Models/BaseCustomerModel.cs b/DeveloperTest/Models/BaseCustomerModel.cs new file mode 100644 index 0000000..04087fa --- /dev/null +++ b/DeveloperTest/Models/BaseCustomerModel.cs @@ -0,0 +1,11 @@ +using DeveloperTest.Database.Models; + +namespace DeveloperTest.Models +{ + public class BaseCustomerModel + { + public string Name { get; set; } + + public CustomerType Type { get; set; } + } +} diff --git a/DeveloperTest/Models/CustomerModel.cs b/DeveloperTest/Models/CustomerModel.cs new file mode 100644 index 0000000..400de07 --- /dev/null +++ b/DeveloperTest/Models/CustomerModel.cs @@ -0,0 +1,13 @@ +using DeveloperTest.Database.Models; + +namespace DeveloperTest.Models +{ + public class CustomerModel + { + public int CustomerId { get; set; } + + public string Name { get; set; } + + public CustomerType Type { get; set; } + } +} diff --git a/DeveloperTest/Startup.cs b/DeveloperTest/Startup.cs index 5242f22..64012da 100644 --- a/DeveloperTest/Startup.cs +++ b/DeveloperTest/Startup.cs @@ -28,6 +28,7 @@ public void ConfigureServices(IServiceCollection services) options.UseSqlServer(Configuration.GetConnectionString("DefaultConnection"))); services.AddTransient(); + services.AddTransient(); } // This method gets called by the runtime. Use this method to configure the HTTP request pipeline. From 90386139159fba6b974444c00048dd055ff69524 Mon Sep 17 00:00:00 2001 From: "pientka.k" Date: Sun, 27 Mar 2022 13:23:55 +0200 Subject: [PATCH 04/21] added readme --- DeveloperTest/readme.md | 1 + 1 file changed, 1 insertion(+) create mode 100644 DeveloperTest/readme.md diff --git a/DeveloperTest/readme.md b/DeveloperTest/readme.md new file mode 100644 index 0000000..3852a64 --- /dev/null +++ b/DeveloperTest/readme.md @@ -0,0 +1 @@ +# implement repository pattern to decouple data access layer From 2f858be0774d0c0aa0b5ec0b3673afb619ef9b33 Mon Sep 17 00:00:00 2001 From: "pientka.k" Date: Sun, 27 Mar 2022 20:14:16 +0200 Subject: [PATCH 05/21] added endpoint for returning customer types; added enum conversion, error handling --- DeveloperTest/Business/CustomerService.cs | 28 ++++++++++++++++--- .../Business/Interfaces/ICustomerService.cs | 7 +++-- .../Controllers/CustomerController.cs | 12 ++++++++ 3 files changed, 40 insertions(+), 7 deletions(-) diff --git a/DeveloperTest/Business/CustomerService.cs b/DeveloperTest/Business/CustomerService.cs index 50888ac..0a223f3 100644 --- a/DeveloperTest/Business/CustomerService.cs +++ b/DeveloperTest/Business/CustomerService.cs @@ -3,6 +3,9 @@ using DeveloperTest.Database.Models; using DeveloperTest.Mappers; using DeveloperTest.Models; +using Microsoft.EntityFrameworkCore; +using System; +using System.Linq; using System.Threading.Tasks; namespace DeveloperTest.Business @@ -16,12 +19,17 @@ public CustomerService(ApplicationDbContext context) this.context = context; } - public async Task CreateCustomerAsync(BaseCustomerModel model) + public async ValueTask CreateCustomerAsync(BaseCustomerModel model) { + if (!Enum.TryParse(value: model.Type, ignoreCase: true, out CustomerType result)) + { + throw new ArgumentException($"{nameof(CustomerType)} must be valid."); + } + var customer = context.Customers.Add(new Customer { Name = model.Name, - Type = model.Type + Type = result }); await context.SaveChangesAsync(); @@ -37,9 +45,21 @@ public async Task GetCustomerAsync(int id) return customer?.ToModel(); } - public Task GetCustomersAsync() + public async Task GetCustomersAsync() + { + return await context.Customers + .Select(customer => new CustomerModel + { + CustomerId = customer.CustomerId, + Name = customer.Name, + Type = customer.Type + }) + .ToArrayAsync(); + } + + public string[] GetTypes() { - throw new System.NotImplementedException(); + return typeof(CustomerType).GetEnumNames(); } } } diff --git a/DeveloperTest/Business/Interfaces/ICustomerService.cs b/DeveloperTest/Business/Interfaces/ICustomerService.cs index 75eb44b..0fbfce8 100644 --- a/DeveloperTest/Business/Interfaces/ICustomerService.cs +++ b/DeveloperTest/Business/Interfaces/ICustomerService.cs @@ -5,8 +5,9 @@ namespace DeveloperTest.Business.Interfaces { public interface ICustomerService { - public Task GetCustomersAsync(); - public Task GetCustomerAsync(int id); - public Task CreateCustomerAsync(BaseCustomerModel model); + Task GetCustomersAsync(); + Task GetCustomerAsync(int id); + ValueTask CreateCustomerAsync(BaseCustomerModel model); + string[] GetTypes(); } } diff --git a/DeveloperTest/Controllers/CustomerController.cs b/DeveloperTest/Controllers/CustomerController.cs index 24c6ff1..4fb8f47 100644 --- a/DeveloperTest/Controllers/CustomerController.cs +++ b/DeveloperTest/Controllers/CustomerController.cs @@ -3,6 +3,7 @@ using DeveloperTest.Business.Interfaces; using DeveloperTest.Models; using System.Threading.Tasks; +using DeveloperTest.Database.Models; namespace DeveloperTest.Controllers { @@ -44,9 +45,20 @@ public async ValueTask Create(BaseCustomerModel model) return BadRequest($"Customer name cannot have less than {minCustomerNameLength} characters"); } + if (!Enum.TryParse(value: model.Type, ignoreCase: true, out CustomerType result)) + { + return BadRequest($"Customer type must be chosen."); + } + var customer = await customerService.CreateCustomerAsync(model); return Created($"customer/{customer.CustomerId}", customer); } + + [HttpGet("types")] + public IActionResult GetTypes() + { + return Ok(customerService.GetTypes()); + } } } \ No newline at end of file From 2824c0470af503e19467ad82e0bc9f54bdea98bf Mon Sep 17 00:00:00 2001 From: "pientka.k" Date: Sun, 27 Mar 2022 20:14:51 +0200 Subject: [PATCH 06/21] type is now string for proper error handling --- DeveloperTest/Models/BaseCustomerModel.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/DeveloperTest/Models/BaseCustomerModel.cs b/DeveloperTest/Models/BaseCustomerModel.cs index 04087fa..01aaba0 100644 --- a/DeveloperTest/Models/BaseCustomerModel.cs +++ b/DeveloperTest/Models/BaseCustomerModel.cs @@ -6,6 +6,6 @@ public class BaseCustomerModel { public string Name { get; set; } - public CustomerType Type { get; set; } + public string Type { get; set; } } } From 08bf81b4a09a970a2cdeb8a58bf368d2ef9756f6 Mon Sep 17 00:00:00 2001 From: "pientka.k" Date: Sun, 27 Mar 2022 20:14:59 +0200 Subject: [PATCH 07/21] remvoed obsolete method --- DeveloperTest/Mappers/CustomerMappingExtensions.cs | 9 --------- 1 file changed, 9 deletions(-) diff --git a/DeveloperTest/Mappers/CustomerMappingExtensions.cs b/DeveloperTest/Mappers/CustomerMappingExtensions.cs index 3f92469..7338f98 100644 --- a/DeveloperTest/Mappers/CustomerMappingExtensions.cs +++ b/DeveloperTest/Mappers/CustomerMappingExtensions.cs @@ -15,15 +15,6 @@ public static Customer ToCustomerDbEntity(this CustomerModel source) }; } - public static BaseCustomerModel ToBaseCustomerModel(this CustomerModel source) - { - return new BaseCustomerModel() - { - Name = source.Name, - Type = source.Type - }; - } - public static CustomerModel ToModel(this Customer source) { return new CustomerModel() From 182fc954c367608638184156e3c5453b72b390ff Mon Sep 17 00:00:00 2001 From: "pientka.k" Date: Sun, 27 Mar 2022 20:15:24 +0200 Subject: [PATCH 08/21] added ef console logging for developement environment --- DeveloperTest/Startup.cs | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/DeveloperTest/Startup.cs b/DeveloperTest/Startup.cs index 64012da..62832ef 100644 --- a/DeveloperTest/Startup.cs +++ b/DeveloperTest/Startup.cs @@ -7,6 +7,7 @@ using DeveloperTest.Business; using DeveloperTest.Business.Interfaces; using DeveloperTest.Database; +using System; namespace DeveloperTest { @@ -25,7 +26,16 @@ public void ConfigureServices(IServiceCollection services) services.AddControllers(); services.AddDbContext(options => - options.UseSqlServer(Configuration.GetConnectionString("DefaultConnection"))); + { + options.UseSqlServer(Configuration.GetConnectionString("DefaultConnection")); + + var environment = Environment.GetEnvironmentVariable("ASPNETCORE_ENVIRONMENT"); + var isDevelopment = environment == Environments.Development; + if (isDevelopment) + { + options.LogTo(Console.WriteLine); + } + }); services.AddTransient(); services.AddTransient(); From 238200bdd7cf01f05e0556ebade5b0af4317bcf7 Mon Sep 17 00:00:00 2001 From: "pientka.k" Date: Sun, 27 Mar 2022 20:15:54 +0200 Subject: [PATCH 09/21] added task functional requirements --- DeveloperTest/readme.md | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/DeveloperTest/readme.md b/DeveloperTest/readme.md index 3852a64..732607f 100644 --- a/DeveloperTest/readme.md +++ b/DeveloperTest/readme.md @@ -1 +1,15 @@ # implement repository pattern to decouple data access layer + +1. Create a screen that allows a user to manage a list of Customers, the Customer should have a Name and a Type. + [] Name is required and must have a minimum length of 5 characters + [+] Type is required and should be a select box of either "Large" or "Small". + [+] There should be a form to add a new customer (like on the jobs page). + [+] There should be a list to see all the customers (like on the jobs page). + [] There should be a link on the list to open the customer record (like on the jobs page). + +2. The jobs page needs to be extended to allow assigning a job to a customer. + [] When creating a job the user should be able to pick a customer from the dropdown. + [] Selecting a customer should be required for creating a job. + [] The user should be able to see assigned customer in the jobs list. + [] For any existing jobs that were not assigned to a customer it should display "Unknown" in the list. + [] When the user opens the job details from the list this screen should include information about the Customer - Name and Type. \ No newline at end of file From 42ffe8afc4c151c83443634d90f88049398c2947 Mon Sep 17 00:00:00 2001 From: "pientka.k" Date: Sun, 27 Mar 2022 20:26:50 +0200 Subject: [PATCH 10/21] added initial customer handling --- ui/src/app/app-routing.module.ts | 6 ++- ui/src/app/app.component.html | 1 + ui/src/app/app.module.ts | 6 ++- .../customer-detail.component.html | 5 +++ .../customer-detail.component.scss | 0 .../customer-detail.component.spec.ts | 25 +++++++++++ .../customer-detail.component.ts | 27 ++++++++++++ ui/src/app/customer/customer.component.html | 33 ++++++++++++++ ui/src/app/customer/customer.component.scss | 43 +++++++++++++++++++ .../app/customer/customer.component.spec.ts | 25 +++++++++++ ui/src/app/customer/customer.component.ts | 40 +++++++++++++++++ ui/src/app/models/customer.model.ts | 10 +++++ ui/src/app/services/customer.service.spec.ts | 16 +++++++ ui/src/app/services/customer.service.ts | 29 +++++++++++++ ui/src/environments/environment.ts | 3 +- 15 files changed, 266 insertions(+), 3 deletions(-) create mode 100644 ui/src/app/customer-detail/customer-detail.component.html create mode 100644 ui/src/app/customer-detail/customer-detail.component.scss create mode 100644 ui/src/app/customer-detail/customer-detail.component.spec.ts create mode 100644 ui/src/app/customer-detail/customer-detail.component.ts create mode 100644 ui/src/app/customer/customer.component.html create mode 100644 ui/src/app/customer/customer.component.scss create mode 100644 ui/src/app/customer/customer.component.spec.ts create mode 100644 ui/src/app/customer/customer.component.ts create mode 100644 ui/src/app/models/customer.model.ts create mode 100644 ui/src/app/services/customer.service.spec.ts create mode 100644 ui/src/app/services/customer.service.ts diff --git a/ui/src/app/app-routing.module.ts b/ui/src/app/app-routing.module.ts index a6af4f8..90b239e 100644 --- a/ui/src/app/app-routing.module.ts +++ b/ui/src/app/app-routing.module.ts @@ -3,13 +3,17 @@ import { Routes, RouterModule } from '@angular/router'; import { JobComponent } from './job/job.component'; import { HomeComponent } from './home/home.component'; import { JobDetailComponent } from './job-detail/job-detail.component'; +import { CustomerComponent } from './customer/customer.component'; +import { CustomerDetailComponent } from './customer-detail/customer-detail.component'; const routes: Routes = [ { path: '', component: HomeComponent }, { path: 'home', component: HomeComponent }, { path: 'jobs', component: JobComponent }, - { path: 'job/:id', component: JobDetailComponent } + { path: 'job/:id', component: JobDetailComponent }, + { path: 'customers', component: CustomerComponent }, + { path: 'customer/:id', component: CustomerDetailComponent } ]; @NgModule({ diff --git a/ui/src/app/app.component.html b/ui/src/app/app.component.html index 49133ec..94af8ff 100644 --- a/ui/src/app/app.component.html +++ b/ui/src/app/app.component.html @@ -2,6 +2,7 @@ \ No newline at end of file diff --git a/ui/src/app/app.module.ts b/ui/src/app/app.module.ts index 0d1f678..6b95279 100644 --- a/ui/src/app/app.module.ts +++ b/ui/src/app/app.module.ts @@ -8,13 +8,17 @@ import { AppComponent } from './app.component'; import { JobComponent } from './job/job.component'; import { HomeComponent } from './home/home.component'; import { JobDetailComponent } from './job-detail/job-detail.component'; +import { CustomerComponent } from './customer/customer.component'; +import { CustomerDetailComponent } from './customer-detail/customer-detail.component'; @NgModule({ declarations: [ AppComponent, JobComponent, HomeComponent, - JobDetailComponent + JobDetailComponent, + CustomerComponent, + CustomerDetailComponent ], imports: [ FormsModule, diff --git a/ui/src/app/customer-detail/customer-detail.component.html b/ui/src/app/customer-detail/customer-detail.component.html new file mode 100644 index 0000000..d72d4c2 --- /dev/null +++ b/ui/src/app/customer-detail/customer-detail.component.html @@ -0,0 +1,5 @@ +

CustomerId: {{customer.customerId}}

+

Name: {{customer.name}}

+

Customer type: {{customer.type}}

+ +Back \ No newline at end of file diff --git a/ui/src/app/customer-detail/customer-detail.component.scss b/ui/src/app/customer-detail/customer-detail.component.scss new file mode 100644 index 0000000..e69de29 diff --git a/ui/src/app/customer-detail/customer-detail.component.spec.ts b/ui/src/app/customer-detail/customer-detail.component.spec.ts new file mode 100644 index 0000000..8ae027e --- /dev/null +++ b/ui/src/app/customer-detail/customer-detail.component.spec.ts @@ -0,0 +1,25 @@ +import { ComponentFixture, TestBed } from '@angular/core/testing'; + +import { CustomerDetailComponent } from './customer-detail.component'; + +describe('CustomerDetailComponent', () => { + let component: CustomerDetailComponent; + let fixture: ComponentFixture; + + beforeEach(async () => { + await TestBed.configureTestingModule({ + declarations: [ CustomerDetailComponent ] + }) + .compileComponents(); + }); + + beforeEach(() => { + fixture = TestBed.createComponent(CustomerDetailComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); +}); diff --git a/ui/src/app/customer-detail/customer-detail.component.ts b/ui/src/app/customer-detail/customer-detail.component.ts new file mode 100644 index 0000000..a453831 --- /dev/null +++ b/ui/src/app/customer-detail/customer-detail.component.ts @@ -0,0 +1,27 @@ +import { Component, OnInit } from '@angular/core'; +import { ActivatedRoute } from '@angular/router'; +import { CustomerModel } from '../models/customer.model'; +import { CustomerService } from '../services/customer.service'; + +@Component({ + selector: 'app-customer-detail', + templateUrl: './customer-detail.component.html', + styleUrls: ['./customer-detail.component.scss'] +}) +export class CustomerDetailComponent implements OnInit { + + public customerId: number; + public customer: CustomerModel; + + constructor( + private route: ActivatedRoute, + private customerService: CustomerService) { + this.customerId = route.snapshot.params.id; + } + + ngOnInit(): void { + this.customerService.GetCustomer(this.customerId) + .subscribe(customer => this.customer = customer); + } + +} diff --git a/ui/src/app/customer/customer.component.html b/ui/src/app/customer/customer.component.html new file mode 100644 index 0000000..c381c18 --- /dev/null +++ b/ui/src/app/customer/customer.component.html @@ -0,0 +1,33 @@ +

New customer form

+
+ + + Please select a valid date + + + Please select a customer type + +
+ +

Customers list

+ + + + + + + + + + + + + + + +
NameCustomer type
{{customer.name}}{{customer.type}} + Open +
\ No newline at end of file diff --git a/ui/src/app/customer/customer.component.scss b/ui/src/app/customer/customer.component.scss new file mode 100644 index 0000000..2591986 --- /dev/null +++ b/ui/src/app/customer/customer.component.scss @@ -0,0 +1,43 @@ +h2 { + margin-left: 15px; +} + +form { + margin: 15px; + + label { + display: block; + } + + input, + select, + button { + display: block; + width: 250px; + margin-bottom: 15px; + } + + small { + color: red; + margin-top: -12px; + margin-bottom: 15px; + display: block; + } +} + +table { + margin: 15px; + border-collapse: collapse; + + th, + td { + border: 1px solid #ddd; + padding: 5px; + min-width: 100px; + text-align: left; + } + + th { + background-color: #ddd; + } +} diff --git a/ui/src/app/customer/customer.component.spec.ts b/ui/src/app/customer/customer.component.spec.ts new file mode 100644 index 0000000..2092a8c --- /dev/null +++ b/ui/src/app/customer/customer.component.spec.ts @@ -0,0 +1,25 @@ +import { ComponentFixture, TestBed } from '@angular/core/testing'; + +import { CustomerComponent } from './customer.component'; + +describe('CustomerComponent', () => { + let component: CustomerComponent; + let fixture: ComponentFixture; + + beforeEach(async () => { + await TestBed.configureTestingModule({ + declarations: [ CustomerComponent ] + }) + .compileComponents(); + }); + + beforeEach(() => { + fixture = TestBed.createComponent(CustomerComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); +}); diff --git a/ui/src/app/customer/customer.component.ts b/ui/src/app/customer/customer.component.ts new file mode 100644 index 0000000..e6ef1ac --- /dev/null +++ b/ui/src/app/customer/customer.component.ts @@ -0,0 +1,40 @@ +import { Component, OnInit } from '@angular/core'; +import { NgForm } from '@angular/forms'; +import { CustomerModel } from '../models/customer.model'; +import { CustomerService } from '../services/customer.service'; + +@Component({ + selector: 'app-customer', + templateUrl: './customer.component.html', + styleUrls: ['./customer.component.scss'] +}) +export class CustomerComponent implements OnInit { + + public customerTypes: string[] = []; + public customers: CustomerModel[] = []; + public newCustomer: CustomerModel = { + customerId: null, + name: null, + type: null + }; + + constructor(private customerService: CustomerService) { } + + ngOnInit(): void { + this.customerService.GetCustomers() + .subscribe(customers => this.customers = customers); + this.customerService.GetCustomerTypes() + .subscribe(customerTypes => this.customerTypes = customerTypes); + } + + public createCustomer(form: NgForm): void { + if (form.invalid) { + alert('form is not valid'); + } else { + this.customerService.CreateCustomer(this.newCustomer).then(() => { + this.customerService.GetCustomers().subscribe(jobs => this.customers = jobs); + }); + } + } + +} diff --git a/ui/src/app/models/customer.model.ts b/ui/src/app/models/customer.model.ts new file mode 100644 index 0000000..ca50302 --- /dev/null +++ b/ui/src/app/models/customer.model.ts @@ -0,0 +1,10 @@ +export interface CustomerModel { + customerId: number; + name: string; + type: CustomerType; +} + +export enum CustomerType { + Small, + Large +} diff --git a/ui/src/app/services/customer.service.spec.ts b/ui/src/app/services/customer.service.spec.ts new file mode 100644 index 0000000..adabb97 --- /dev/null +++ b/ui/src/app/services/customer.service.spec.ts @@ -0,0 +1,16 @@ +import { TestBed } from '@angular/core/testing'; + +import { CustomerService } from './customer.service'; + +describe('CustomerService', () => { + let service: CustomerService; + + beforeEach(() => { + TestBed.configureTestingModule({}); + service = TestBed.inject(CustomerService); + }); + + it('should be created', () => { + expect(service).toBeTruthy(); + }); +}); diff --git a/ui/src/app/services/customer.service.ts b/ui/src/app/services/customer.service.ts new file mode 100644 index 0000000..00ee164 --- /dev/null +++ b/ui/src/app/services/customer.service.ts @@ -0,0 +1,29 @@ +import { HttpClient } from '@angular/common/http'; +import { Injectable } from '@angular/core'; +import { Observable } from 'rxjs/internal/Observable'; +import { environment } from 'src/environments/environment'; +import { CustomerModel } from '../models/customer.model'; + +@Injectable({ + providedIn: 'root' +}) + +export class CustomerService { + constructor(private httpClient: HttpClient) { } + + public GetCustomerTypes(): Observable { + return this.httpClient.get(environment.apiUrl + '/customer/types'); + } + + public GetCustomers(): Observable { + return this.httpClient.get(environment.apiUrl + '/customer'); + } + + public GetCustomer(customerId: number): Observable { + return this.httpClient.get(environment.apiUrl + `/customer/${customerId}`); + } + + public CreateCustomer(customer: CustomerModel): Promise { + return this.httpClient.post(environment.apiUrl + '/customer', customer).toPromise(); + } +} diff --git a/ui/src/environments/environment.ts b/ui/src/environments/environment.ts index 7b4f817..6433347 100644 --- a/ui/src/environments/environment.ts +++ b/ui/src/environments/environment.ts @@ -3,7 +3,8 @@ // The list of file replacements can be found in `angular.json`. export const environment = { - production: false + production: false, + apiUrl: "http://localhost:63235" }; /* From 5c17dd9fee15929ada2d156b9f19e51b106f8800 Mon Sep 17 00:00:00 2001 From: "pientka.k" Date: Sun, 27 Mar 2022 20:42:18 +0200 Subject: [PATCH 11/21] udpated @types/node --- ui/package-lock.json | 28 +++++++--------------------- ui/package.json | 4 ++-- 2 files changed, 9 insertions(+), 23 deletions(-) diff --git a/ui/package-lock.json b/ui/package-lock.json index abc8434..35b49aa 100644 --- a/ui/package-lock.json +++ b/ui/package-lock.json @@ -28,7 +28,7 @@ "@angular/language-service": "~13.2.0", "@types/jasmine": "~3.3.8", "@types/jasminewd2": "~2.0.3", - "@types/node": "~8.9.4", + "@types/node": "^17.0.23", "codelyzer": "^6.0.0", "jasmine-core": "~3.4.0", "jasmine-spec-reporter": "~4.2.1", @@ -2519,9 +2519,9 @@ "dev": true }, "node_modules/@types/node": { - "version": "8.9.5", - "resolved": "https://registry.npmjs.org/@types/node/-/node-8.9.5.tgz", - "integrity": "sha512-jRHfWsvyMtXdbhnz5CVHxaBgnV6duZnPlQuRSo/dm/GnmikNcmZhxIES4E9OZjUmQ8C+HCl4KJux+cXN/ErGDQ==", + "version": "17.0.23", + "resolved": "https://registry.npmjs.org/@types/node/-/node-17.0.23.tgz", + "integrity": "sha512-UxDxWn7dl97rKVeVS61vErvw086aCYhDLyvRQZ5Rk65rZKepaFdm53GeqXaKBuOhED4e9uWq34IC3TdSdJJ2Gw==", "dev": true }, "node_modules/@types/parse-json": { @@ -5032,12 +5032,6 @@ "node": ">=10.0.0" } }, - "node_modules/engine.io/node_modules/@types/node": { - "version": "17.0.14", - "resolved": "https://registry.npmjs.org/@types/node/-/node-17.0.14.tgz", - "integrity": "sha512-SbjLmERksKOGzWzPNuW7fJM7fk3YXVTFiZWB/Hs99gwhk+/dnrQRPBQjPW9aO+fi1tAffi9PrwFvsmOKmDTyng==", - "dev": true - }, "node_modules/enhanced-resolve": { "version": "5.8.3", "resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-5.8.3.tgz", @@ -14649,9 +14643,9 @@ "dev": true }, "@types/node": { - "version": "8.9.5", - "resolved": "https://registry.npmjs.org/@types/node/-/node-8.9.5.tgz", - "integrity": "sha512-jRHfWsvyMtXdbhnz5CVHxaBgnV6duZnPlQuRSo/dm/GnmikNcmZhxIES4E9OZjUmQ8C+HCl4KJux+cXN/ErGDQ==", + "version": "17.0.23", + "resolved": "https://registry.npmjs.org/@types/node/-/node-17.0.23.tgz", + "integrity": "sha512-UxDxWn7dl97rKVeVS61vErvw086aCYhDLyvRQZ5Rk65rZKepaFdm53GeqXaKBuOhED4e9uWq34IC3TdSdJJ2Gw==", "dev": true }, "@types/parse-json": { @@ -16628,14 +16622,6 @@ "debug": "~4.3.1", "engine.io-parser": "~5.0.0", "ws": "~8.2.3" - }, - "dependencies": { - "@types/node": { - "version": "17.0.14", - "resolved": "https://registry.npmjs.org/@types/node/-/node-17.0.14.tgz", - "integrity": "sha512-SbjLmERksKOGzWzPNuW7fJM7fk3YXVTFiZWB/Hs99gwhk+/dnrQRPBQjPW9aO+fi1tAffi9PrwFvsmOKmDTyng==", - "dev": true - } } }, "engine.io-parser": { diff --git a/ui/package.json b/ui/package.json index f0f1942..71db3fb 100644 --- a/ui/package.json +++ b/ui/package.json @@ -31,7 +31,7 @@ "@angular/language-service": "~13.2.0", "@types/jasmine": "~3.3.8", "@types/jasminewd2": "~2.0.3", - "@types/node": "~8.9.4", + "@types/node": "^17.0.23", "codelyzer": "^6.0.0", "jasmine-core": "~3.4.0", "jasmine-spec-reporter": "~4.2.1", @@ -45,4 +45,4 @@ "tslint": "~6.1.3", "typescript": "~4.5.5" } -} \ No newline at end of file +} From 0588156085b2c5fc01e5a3ccf22a388a81cbb551 Mon Sep 17 00:00:00 2001 From: "pientka.k" Date: Sun, 27 Mar 2022 20:42:31 +0200 Subject: [PATCH 12/21] handing of subscriptions --- .../customer-detail.component.ts | 8 +++++++- ui/src/app/customer/customer.component.ts | 20 ++++++++++++++----- 2 files changed, 22 insertions(+), 6 deletions(-) diff --git a/ui/src/app/customer-detail/customer-detail.component.ts b/ui/src/app/customer-detail/customer-detail.component.ts index a453831..21328b5 100644 --- a/ui/src/app/customer-detail/customer-detail.component.ts +++ b/ui/src/app/customer-detail/customer-detail.component.ts @@ -1,5 +1,6 @@ import { Component, OnInit } from '@angular/core'; import { ActivatedRoute } from '@angular/router'; +import { Subscription } from 'rxjs/internal/Subscription'; import { CustomerModel } from '../models/customer.model'; import { CustomerService } from '../services/customer.service'; @@ -12,6 +13,7 @@ export class CustomerDetailComponent implements OnInit { public customerId: number; public customer: CustomerModel; + sub: Subscription | undefined; constructor( private route: ActivatedRoute, @@ -20,8 +22,12 @@ export class CustomerDetailComponent implements OnInit { } ngOnInit(): void { - this.customerService.GetCustomer(this.customerId) + this.sub = this.customerService.GetCustomer(this.customerId) .subscribe(customer => this.customer = customer); } + ngOnDestroy() { + this.sub?.unsubscribe(); + } + } diff --git a/ui/src/app/customer/customer.component.ts b/ui/src/app/customer/customer.component.ts index e6ef1ac..6281285 100644 --- a/ui/src/app/customer/customer.component.ts +++ b/ui/src/app/customer/customer.component.ts @@ -1,5 +1,7 @@ +import { forEach } from '@angular-devkit/schematics'; import { Component, OnInit } from '@angular/core'; import { NgForm } from '@angular/forms'; +import { Subscription } from 'rxjs/internal/Subscription'; import { CustomerModel } from '../models/customer.model'; import { CustomerService } from '../services/customer.service'; @@ -10,6 +12,7 @@ import { CustomerService } from '../services/customer.service'; }) export class CustomerComponent implements OnInit { + subs: Subscription[] | undefined; public customerTypes: string[] = []; public customers: CustomerModel[] = []; public newCustomer: CustomerModel = { @@ -21,10 +24,11 @@ export class CustomerComponent implements OnInit { constructor(private customerService: CustomerService) { } ngOnInit(): void { - this.customerService.GetCustomers() - .subscribe(customers => this.customers = customers); - this.customerService.GetCustomerTypes() - .subscribe(customerTypes => this.customerTypes = customerTypes); + this.subs.push(this.customerService.GetCustomers() + .subscribe(customers => this.customers = customers)); + + this.subs.push(this.customerService.GetCustomerTypes() + .subscribe(customerTypes => this.customerTypes = customerTypes)); } public createCustomer(form: NgForm): void { @@ -32,9 +36,15 @@ export class CustomerComponent implements OnInit { alert('form is not valid'); } else { this.customerService.CreateCustomer(this.newCustomer).then(() => { - this.customerService.GetCustomers().subscribe(jobs => this.customers = jobs); + this.subs.push(this.customerService.GetCustomers().subscribe(jobs => this.customers = jobs)); }); } } + ngOnDestroy() { + this.subs.forEach(sub => { + sub?.unsubscribe(); + }); + } + } From a2648d52d1dc886f348de0eb801721c4844282cc Mon Sep 17 00:00:00 2001 From: "pientka.k" Date: Fri, 1 Apr 2022 20:58:23 +0200 Subject: [PATCH 13/21] added previous migrations after db wipe with additional nullable foreing key to customer in job table --- .../Database/ApplicationDbContext.cs | 34 +++--- DeveloperTest/Database/Models/Customer.cs | 5 +- DeveloperTest/Database/Models/Job.cs | 4 +- ...0331182019_AddedCustomerTable.Designer.cs} | 4 +- ...s => 20220331182019_AddedCustomerTable.cs} | 2 +- ...dJobNullableCustomerIdRelation.Designer.cs | 102 ++++++++++++++++++ ...3035_AddedJobNullableCustomerIdRelation.cs | 45 ++++++++ .../ApplicationDbContextModelSnapshot.cs | 19 ++++ DeveloperTest/Models/BaseJobModel.cs | 2 +- DeveloperTest/Models/JobModel.cs | 4 +- 10 files changed, 197 insertions(+), 24 deletions(-) rename DeveloperTest/Migrations/{20220327080819_AddCustomersTable.Designer.cs => 20220331182019_AddedCustomerTable.Designer.cs} (96%) rename DeveloperTest/Migrations/{20220327080819_AddCustomersTable.cs => 20220331182019_AddedCustomerTable.cs} (95%) create mode 100644 DeveloperTest/Migrations/20220401183035_AddedJobNullableCustomerIdRelation.Designer.cs create mode 100644 DeveloperTest/Migrations/20220401183035_AddedJobNullableCustomerIdRelation.cs diff --git a/DeveloperTest/Database/ApplicationDbContext.cs b/DeveloperTest/Database/ApplicationDbContext.cs index 61da997..bf8f624 100644 --- a/DeveloperTest/Database/ApplicationDbContext.cs +++ b/DeveloperTest/Database/ApplicationDbContext.cs @@ -16,21 +16,6 @@ public ApplicationDbContext(DbContextOptions options) : ba protected override void OnModelCreating(ModelBuilder modelBuilder) { - modelBuilder.Entity() - .HasKey(x => x.JobId); - - modelBuilder.Entity() - .Property(x => x.JobId) - .ValueGeneratedOnAdd(); - - modelBuilder.Entity() - .HasData(new Job - { - JobId = 1, - Engineer = "Test", - When = new DateTime(2022, 2, 1, 12, 0, 0) - }); - modelBuilder.Entity() .HasKey(x => x.CustomerId); @@ -46,6 +31,25 @@ protected override void OnModelCreating(ModelBuilder modelBuilder) Type = CustomerType.Small }); + modelBuilder.Entity() + .HasKey(x => x.JobId); + + modelBuilder.Entity() + .Property(x => x.JobId) + .ValueGeneratedOnAdd(); + + modelBuilder.Entity() + .HasOne(j => j.Customer) + .WithMany(c => c.Jobs); + + modelBuilder.Entity() + .HasData(new Job + { + JobId = 1, + Engineer = "Test", + When = new DateTime(2022, 2, 1, 12, 0, 0) + }); + base.OnModelCreating(modelBuilder); } diff --git a/DeveloperTest/Database/Models/Customer.cs b/DeveloperTest/Database/Models/Customer.cs index d6225f2..529fa8a 100644 --- a/DeveloperTest/Database/Models/Customer.cs +++ b/DeveloperTest/Database/Models/Customer.cs @@ -1,10 +1,13 @@ -namespace DeveloperTest.Database.Models +using System.Collections.Generic; + +namespace DeveloperTest.Database.Models { public class Customer { public int CustomerId { get; set; } public string Name { get; set; } public CustomerType Type { get; set; } + public virtual ICollection Jobs { get; set; } } public enum CustomerType diff --git a/DeveloperTest/Database/Models/Job.cs b/DeveloperTest/Database/Models/Job.cs index 8a2abd0..fe0e6a1 100644 --- a/DeveloperTest/Database/Models/Job.cs +++ b/DeveloperTest/Database/Models/Job.cs @@ -5,9 +5,9 @@ namespace DeveloperTest.Database.Models public class Job { public int JobId { get; set; } - public string Engineer { get; set; } - public DateTime When { get; set; } + public int? CustomerId { get; set; } + public virtual Customer Customer { get; set; } } } diff --git a/DeveloperTest/Migrations/20220327080819_AddCustomersTable.Designer.cs b/DeveloperTest/Migrations/20220331182019_AddedCustomerTable.Designer.cs similarity index 96% rename from DeveloperTest/Migrations/20220327080819_AddCustomersTable.Designer.cs rename to DeveloperTest/Migrations/20220331182019_AddedCustomerTable.Designer.cs index 9b90d44..1ce984d 100644 --- a/DeveloperTest/Migrations/20220327080819_AddCustomersTable.Designer.cs +++ b/DeveloperTest/Migrations/20220331182019_AddedCustomerTable.Designer.cs @@ -12,8 +12,8 @@ namespace DeveloperTest.Migrations { [DbContext(typeof(ApplicationDbContext))] - [Migration("20220327080819_AddCustomersTable")] - partial class AddCustomersTable + [Migration("20220331182019_AddedCustomerTable")] + partial class AddedCustomerTable { protected override void BuildTargetModel(ModelBuilder modelBuilder) { diff --git a/DeveloperTest/Migrations/20220327080819_AddCustomersTable.cs b/DeveloperTest/Migrations/20220331182019_AddedCustomerTable.cs similarity index 95% rename from DeveloperTest/Migrations/20220327080819_AddCustomersTable.cs rename to DeveloperTest/Migrations/20220331182019_AddedCustomerTable.cs index 82fa7d3..6ec6fb3 100644 --- a/DeveloperTest/Migrations/20220327080819_AddCustomersTable.cs +++ b/DeveloperTest/Migrations/20220331182019_AddedCustomerTable.cs @@ -4,7 +4,7 @@ namespace DeveloperTest.Migrations { - public partial class AddCustomersTable : Migration + public partial class AddedCustomerTable : Migration { protected override void Up(MigrationBuilder migrationBuilder) { diff --git a/DeveloperTest/Migrations/20220401183035_AddedJobNullableCustomerIdRelation.Designer.cs b/DeveloperTest/Migrations/20220401183035_AddedJobNullableCustomerIdRelation.Designer.cs new file mode 100644 index 0000000..41c321e --- /dev/null +++ b/DeveloperTest/Migrations/20220401183035_AddedJobNullableCustomerIdRelation.Designer.cs @@ -0,0 +1,102 @@ +// +using System; +using DeveloperTest.Database; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Metadata; +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; + +#nullable disable + +namespace DeveloperTest.Migrations +{ + [DbContext(typeof(ApplicationDbContext))] + [Migration("20220401183035_AddedJobNullableCustomerIdRelation")] + partial class AddedJobNullableCustomerIdRelation + { + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasAnnotation("ProductVersion", "6.0.1") + .HasAnnotation("Relational:MaxIdentifierLength", 128); + + SqlServerModelBuilderExtensions.UseIdentityColumns(modelBuilder, 1L, 1); + + modelBuilder.Entity("DeveloperTest.Database.Models.Customer", b => + { + b.Property("CustomerId") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("CustomerId"), 1L, 1); + + b.Property("Name") + .HasColumnType("nvarchar(max)"); + + b.Property("Type") + .HasColumnType("int"); + + b.HasKey("CustomerId"); + + b.ToTable("Customers"); + + b.HasData( + new + { + CustomerId = 1, + Name = "TestCustomer", + Type = 0 + }); + }); + + modelBuilder.Entity("DeveloperTest.Database.Models.Job", b => + { + b.Property("JobId") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("JobId"), 1L, 1); + + b.Property("CustomerId") + .HasColumnType("int"); + + b.Property("Engineer") + .HasColumnType("nvarchar(max)"); + + b.Property("When") + .HasColumnType("datetime2"); + + b.HasKey("JobId"); + + b.HasIndex("CustomerId"); + + b.ToTable("Jobs"); + + b.HasData( + new + { + JobId = 1, + Engineer = "Test", + When = new DateTime(2022, 2, 1, 12, 0, 0, 0, DateTimeKind.Unspecified) + }); + }); + + modelBuilder.Entity("DeveloperTest.Database.Models.Job", b => + { + b.HasOne("DeveloperTest.Database.Models.Customer", "Customer") + .WithMany("Jobs") + .HasForeignKey("CustomerId"); + + b.Navigation("Customer"); + }); + + modelBuilder.Entity("DeveloperTest.Database.Models.Customer", b => + { + b.Navigation("Jobs"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/DeveloperTest/Migrations/20220401183035_AddedJobNullableCustomerIdRelation.cs b/DeveloperTest/Migrations/20220401183035_AddedJobNullableCustomerIdRelation.cs new file mode 100644 index 0000000..bd24a00 --- /dev/null +++ b/DeveloperTest/Migrations/20220401183035_AddedJobNullableCustomerIdRelation.cs @@ -0,0 +1,45 @@ +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace DeveloperTest.Migrations +{ + public partial class AddedJobNullableCustomerIdRelation : Migration + { + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.AddColumn( + name: "CustomerId", + table: "Jobs", + type: "int", + nullable: true); + + migrationBuilder.CreateIndex( + name: "IX_Jobs_CustomerId", + table: "Jobs", + column: "CustomerId"); + + migrationBuilder.AddForeignKey( + name: "FK_Jobs_Customers_CustomerId", + table: "Jobs", + column: "CustomerId", + principalTable: "Customers", + principalColumn: "CustomerId"); + } + + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropForeignKey( + name: "FK_Jobs_Customers_CustomerId", + table: "Jobs"); + + migrationBuilder.DropIndex( + name: "IX_Jobs_CustomerId", + table: "Jobs"); + + migrationBuilder.DropColumn( + name: "CustomerId", + table: "Jobs"); + } + } +} diff --git a/DeveloperTest/Migrations/ApplicationDbContextModelSnapshot.cs b/DeveloperTest/Migrations/ApplicationDbContextModelSnapshot.cs index 4ec2dd0..a173f3b 100644 --- a/DeveloperTest/Migrations/ApplicationDbContextModelSnapshot.cs +++ b/DeveloperTest/Migrations/ApplicationDbContextModelSnapshot.cs @@ -57,6 +57,9 @@ protected override void BuildModel(ModelBuilder modelBuilder) SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("JobId"), 1L, 1); + b.Property("CustomerId") + .HasColumnType("int"); + b.Property("Engineer") .HasColumnType("nvarchar(max)"); @@ -65,6 +68,8 @@ protected override void BuildModel(ModelBuilder modelBuilder) b.HasKey("JobId"); + b.HasIndex("CustomerId"); + b.ToTable("Jobs"); b.HasData( @@ -75,6 +80,20 @@ protected override void BuildModel(ModelBuilder modelBuilder) When = new DateTime(2022, 2, 1, 12, 0, 0, 0, DateTimeKind.Unspecified) }); }); + + modelBuilder.Entity("DeveloperTest.Database.Models.Job", b => + { + b.HasOne("DeveloperTest.Database.Models.Customer", "Customer") + .WithMany("Jobs") + .HasForeignKey("CustomerId"); + + b.Navigation("Customer"); + }); + + modelBuilder.Entity("DeveloperTest.Database.Models.Customer", b => + { + b.Navigation("Jobs"); + }); #pragma warning restore 612, 618 } } diff --git a/DeveloperTest/Models/BaseJobModel.cs b/DeveloperTest/Models/BaseJobModel.cs index d2bc052..4318963 100644 --- a/DeveloperTest/Models/BaseJobModel.cs +++ b/DeveloperTest/Models/BaseJobModel.cs @@ -5,7 +5,7 @@ namespace DeveloperTest.Models public class BaseJobModel { public string Engineer { get; set; } - public DateTime When { get; set; } + public int? CustomerId { get; set; } } } diff --git a/DeveloperTest/Models/JobModel.cs b/DeveloperTest/Models/JobModel.cs index 8f2b5be..0047801 100644 --- a/DeveloperTest/Models/JobModel.cs +++ b/DeveloperTest/Models/JobModel.cs @@ -5,9 +5,9 @@ namespace DeveloperTest.Models public class JobModel { public int JobId { get; set; } - public string Engineer { get; set; } - + public int? CustomerId { get; set; } + public string CustomerName { get; set; } public DateTime When { get; set; } } } From 8885362f1641f0c974a93e53bce4a3fee0d3f525 Mon Sep 17 00:00:00 2001 From: "pientka.k" Date: Fri, 1 Apr 2022 21:01:00 +0200 Subject: [PATCH 14/21] including customer job query; added missing handling for enumerations to and from backend --- DeveloperTest/Business/CustomerService.cs | 2 +- DeveloperTest/Business/JobService.cs | 42 ++++++++++++------- DeveloperTest/Controllers/JobController.cs | 7 +++- .../Mappers/CustomerMappingExtensions.cs | 7 +++- DeveloperTest/Models/CustomerModel.cs | 2 +- 5 files changed, 41 insertions(+), 19 deletions(-) diff --git a/DeveloperTest/Business/CustomerService.cs b/DeveloperTest/Business/CustomerService.cs index 0a223f3..3d76d6f 100644 --- a/DeveloperTest/Business/CustomerService.cs +++ b/DeveloperTest/Business/CustomerService.cs @@ -52,7 +52,7 @@ public async Task GetCustomersAsync() { CustomerId = customer.CustomerId, Name = customer.Name, - Type = customer.Type + Type = customer.Type.ToString() }) .ToArrayAsync(); } diff --git a/DeveloperTest/Business/JobService.cs b/DeveloperTest/Business/JobService.cs index 7eb8f64..1a56e92 100644 --- a/DeveloperTest/Business/JobService.cs +++ b/DeveloperTest/Business/JobService.cs @@ -1,38 +1,50 @@ -using System.Linq; +using System; +using System.Linq; using DeveloperTest.Business.Interfaces; using DeveloperTest.Database; using DeveloperTest.Database.Models; using DeveloperTest.Models; +using Microsoft.EntityFrameworkCore; namespace DeveloperTest.Business { public class JobService : IJobService { private readonly ApplicationDbContext context; + private readonly ICustomerService customerService; - public JobService(ApplicationDbContext context) + public JobService(ApplicationDbContext context, + ICustomerService customerService) { this.context = context; + this.customerService = customerService; } public JobModel[] GetJobs() { - return context.Jobs.Select(x => new JobModel - { - JobId = x.JobId, - Engineer = x.Engineer, - When = x.When - }).ToArray(); + return context.Jobs + .Include(j => j.Customer) + .Select(x => new JobModel + { + JobId = x.JobId, + Engineer = x.Engineer, + When = x.When, + CustomerName = x.Customer.Name + }).ToArray(); } public JobModel GetJob(int jobId) { - return context.Jobs.Where(x => x.JobId == jobId).Select(x => new JobModel - { - JobId = x.JobId, - Engineer = x.Engineer, - When = x.When - }).SingleOrDefault(); + return context.Jobs + .Include(c => c.Customer) + .Where(x => x.JobId == jobId).Select(x => new JobModel + { + JobId = x.JobId, + Engineer = x.Engineer, + CustomerId = x.CustomerId.Value, + CustomerName = x.Customer.Name, + When = x.When + }).SingleOrDefault(); } public JobModel CreateJob(BaseJobModel model) @@ -40,6 +52,8 @@ public JobModel CreateJob(BaseJobModel model) var addedJob = context.Jobs.Add(new Job { Engineer = model.Engineer, + CustomerId = model.CustomerId.HasValue + ? model.CustomerId.Value : throw new ArgumentException($"{nameof(model.CustomerId)} cannot be null"), When = model.When }); diff --git a/DeveloperTest/Controllers/JobController.cs b/DeveloperTest/Controllers/JobController.cs index 2ce1c0e..e9ad68d 100644 --- a/DeveloperTest/Controllers/JobController.cs +++ b/DeveloperTest/Controllers/JobController.cs @@ -39,7 +39,12 @@ public IActionResult Create(BaseJobModel model) { if (model.When.Date < DateTime.Now.Date) { - return BadRequest("Date cannot be in the past"); + return BadRequest("Date cannot be in the past."); + } + + if (model.CustomerId is null) + { + return BadRequest("Job must have a customer."); } var job = jobService.CreateJob(model); diff --git a/DeveloperTest/Mappers/CustomerMappingExtensions.cs b/DeveloperTest/Mappers/CustomerMappingExtensions.cs index 7338f98..7b7b841 100644 --- a/DeveloperTest/Mappers/CustomerMappingExtensions.cs +++ b/DeveloperTest/Mappers/CustomerMappingExtensions.cs @@ -1,5 +1,6 @@ using DeveloperTest.Database.Models; using DeveloperTest.Models; +using System; namespace DeveloperTest.Mappers { @@ -11,7 +12,9 @@ public static Customer ToCustomerDbEntity(this CustomerModel source) { CustomerId = source.CustomerId, Name = source.Name, - Type = source.Type, + Type = Enum.TryParse(source.Type, out CustomerType type) + ? type + : throw new ArgumentException("Invalid customer type. Cannot parse the customer type."), }; } @@ -21,7 +24,7 @@ public static CustomerModel ToModel(this Customer source) { CustomerId = source.CustomerId, Name = source.Name, - Type = source.Type + Type = source.Type.ToString() }; } } diff --git a/DeveloperTest/Models/CustomerModel.cs b/DeveloperTest/Models/CustomerModel.cs index 400de07..c5b96db 100644 --- a/DeveloperTest/Models/CustomerModel.cs +++ b/DeveloperTest/Models/CustomerModel.cs @@ -8,6 +8,6 @@ public class CustomerModel public string Name { get; set; } - public CustomerType Type { get; set; } + public string Type { get; set; } } } From f08a0c0d8356294dfe4c9139a8846d555217bf34 Mon Sep 17 00:00:00 2001 From: "pientka.k" Date: Fri, 1 Apr 2022 21:01:34 +0200 Subject: [PATCH 15/21] updated todolist --- DeveloperTest/readme.md | 21 +++++++++++++-------- 1 file changed, 13 insertions(+), 8 deletions(-) diff --git a/DeveloperTest/readme.md b/DeveloperTest/readme.md index 732607f..213a168 100644 --- a/DeveloperTest/readme.md +++ b/DeveloperTest/readme.md @@ -1,15 +1,20 @@ -# implement repository pattern to decouple data access layer +Improvement ideas +# implement repository pattern to decouple data access layer + +TODO LIST 1. Create a screen that allows a user to manage a list of Customers, the Customer should have a Name and a Type. - [] Name is required and must have a minimum length of 5 characters + [] FE Name is required and must have a minimum length of 5 characters + [+] BE Name is required and must have a minimum length of 5 characters [+] Type is required and should be a select box of either "Large" or "Small". [+] There should be a form to add a new customer (like on the jobs page). [+] There should be a list to see all the customers (like on the jobs page). - [] There should be a link on the list to open the customer record (like on the jobs page). + [+] There should be a link on the list to open the customer record (like on the jobs page). 2. The jobs page needs to be extended to allow assigning a job to a customer. - [] When creating a job the user should be able to pick a customer from the dropdown. - [] Selecting a customer should be required for creating a job. - [] The user should be able to see assigned customer in the jobs list. - [] For any existing jobs that were not assigned to a customer it should display "Unknown" in the list. - [] When the user opens the job details from the list this screen should include information about the Customer - Name and Type. \ No newline at end of file + [+] When creating a job the user should be able to pick a customer from the dropdown. + [+] FE Selecting a customer should be required for creating a job. + [+] BE Selecting a customer should be required for creating a job. + [+] The user should be able to see assigned customer in the jobs list. + [+] For any existing jobs that were not assigned to a customer it should display "Unknown" in the list. + [+] When the user opens the job details from the list this screen should include information about the Customer - Name and Type. \ No newline at end of file From ab374bcbe78baad68203bda4a481a0ad6dd17671 Mon Sep 17 00:00:00 2001 From: "pientka.k" Date: Fri, 1 Apr 2022 21:01:57 +0200 Subject: [PATCH 16/21] added subscription handling for customer component --- ui/src/app/customer/customer.component.ts | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/ui/src/app/customer/customer.component.ts b/ui/src/app/customer/customer.component.ts index 6281285..c7b8264 100644 --- a/ui/src/app/customer/customer.component.ts +++ b/ui/src/app/customer/customer.component.ts @@ -12,7 +12,7 @@ import { CustomerService } from '../services/customer.service'; }) export class CustomerComponent implements OnInit { - subs: Subscription[] | undefined; + subs: Subscription[] = []; public customerTypes: string[] = []; public customers: CustomerModel[] = []; public newCustomer: CustomerModel = { @@ -24,11 +24,13 @@ export class CustomerComponent implements OnInit { constructor(private customerService: CustomerService) { } ngOnInit(): void { - this.subs.push(this.customerService.GetCustomers() - .subscribe(customers => this.customers = customers)); + const getCustomersSub = this.customerService.GetCustomers() + .subscribe(customers => this.customers = customers); + this.subs.push(getCustomersSub); - this.subs.push(this.customerService.GetCustomerTypes() - .subscribe(customerTypes => this.customerTypes = customerTypes)); + const getCustomerTypeSub = this.customerService.GetCustomerTypes() + .subscribe(customerTypes => this.customerTypes = customerTypes); + this.subs.push(getCustomerTypeSub); } public createCustomer(form: NgForm): void { From 850aa81cc11333037576ec4a02f556c9105f4c69 Mon Sep 17 00:00:00 2001 From: "pientka.k" Date: Fri, 1 Apr 2022 21:06:15 +0200 Subject: [PATCH 17/21] added unknown customer handling; added unsubscribing on ngDestroy --- ui/src/app/job/job.component.html | 9 ++++++++- ui/src/app/job/job.component.ts | 32 +++++++++++++++++++++++++++---- ui/src/app/models/job.model.ts | 2 ++ 3 files changed, 38 insertions(+), 5 deletions(-) diff --git a/ui/src/app/job/job.component.html b/ui/src/app/job/job.component.html index 085c531..a44930d 100644 --- a/ui/src/app/job/job.component.html +++ b/ui/src/app/job/job.component.html @@ -5,7 +5,12 @@

New job form

- Please select an engineer + + + Please select a customer Please select a valid date @@ -17,6 +22,7 @@

Jobs list

Engineer + Customer When @@ -24,6 +30,7 @@

Jobs list

{{job.engineer}} + {{job.customerName ? job.customerName : unknown}} {{job.when | date:'shortDate'}} Open diff --git a/ui/src/app/job/job.component.ts b/ui/src/app/job/job.component.ts index e9de751..85ba351 100644 --- a/ui/src/app/job/job.component.ts +++ b/ui/src/app/job/job.component.ts @@ -3,6 +3,9 @@ import { NgForm } from '@angular/forms'; import { EngineerService } from '../services/engineer.service'; import { JobService } from '../services/job.service'; import { JobModel } from '../models/job.model'; +import { CustomerModel } from '../models/customer.model'; +import { CustomerService } from '../services/customer.service'; +import { Subscription } from 'rxjs/internal/Subscription'; @Component({ selector: 'app-job', @@ -11,23 +14,33 @@ import { JobModel } from '../models/job.model'; }) export class JobComponent implements OnInit { + private subs: Subscription[] = []; public engineers: string[] = []; - + public customers: CustomerModel[] = []; public jobs: JobModel[] = []; - public newJob: JobModel = { jobId: null, engineer: null, + customerId: null, + customerName: null, when: null }; + public selectedCustomer: CustomerModel = undefined; + public readonly unknown = 'Unknown'; constructor( private engineerService: EngineerService, + private customerService: CustomerService, private jobService: JobService) { } ngOnInit() { - this.engineerService.GetEngineers().subscribe(engineers => this.engineers = engineers); - this.jobService.GetJobs().subscribe(jobs => this.jobs = jobs); + const getEngineersSubscription = this.engineerService.GetEngineers().subscribe(engineers => this.engineers = engineers); + this.subs.push(getEngineersSubscription); + const getCustomersSubscription = this.customerService.GetCustomers().subscribe(customers => this.customers = customers); + this.subs.push(getCustomersSubscription); + const getJobsSubscription = this.jobService.GetJobs().subscribe(jobs => this.jobs = jobs); + this.subs.push(getJobsSubscription); + } public createJob(form: NgForm): void { @@ -40,4 +53,15 @@ export class JobComponent implements OnInit { } } + onSelectCustomer(e: any): void { + this.selectedCustomer = e; + this.newJob.customerId = this.selectedCustomer.customerId; + this.newJob.customerName = this.selectedCustomer.name; + } + + ngOnDestroy() { + this.subs.forEach(sub => { + sub?.unsubscribe(); + }); + } } diff --git a/ui/src/app/models/job.model.ts b/ui/src/app/models/job.model.ts index 5c3342c..cc3718f 100644 --- a/ui/src/app/models/job.model.ts +++ b/ui/src/app/models/job.model.ts @@ -1,5 +1,7 @@ export interface JobModel { jobId: number; engineer: string; + customerId: number; + customerName: string; when: Date; } From 12a443e75f7d53c588b5536079440bd7c2a3f1cf Mon Sep 17 00:00:00 2001 From: "pientka.k" Date: Fri, 1 Apr 2022 21:10:40 +0200 Subject: [PATCH 18/21] made accessor private for subscription array --- ui/src/app/customer/customer.component.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ui/src/app/customer/customer.component.ts b/ui/src/app/customer/customer.component.ts index c7b8264..bd00ab2 100644 --- a/ui/src/app/customer/customer.component.ts +++ b/ui/src/app/customer/customer.component.ts @@ -12,7 +12,7 @@ import { CustomerService } from '../services/customer.service'; }) export class CustomerComponent implements OnInit { - subs: Subscription[] = []; + private subs: Subscription[] = []; public customerTypes: string[] = []; public customers: CustomerModel[] = []; public newCustomer: CustomerModel = { From fd8bc0538c6982e3826f5e97032be5e514be7bbc Mon Sep 17 00:00:00 2001 From: "pientka.k" Date: Fri, 1 Apr 2022 21:10:51 +0200 Subject: [PATCH 19/21] added minimum name length validation --- ui/src/app/customer/customer.component.html | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/ui/src/app/customer/customer.component.html b/ui/src/app/customer/customer.component.html index c381c18..9799f53 100644 --- a/ui/src/app/customer/customer.component.html +++ b/ui/src/app/customer/customer.component.html @@ -1,8 +1,8 @@

New customer form

- - Please select a valid date + + Invalid customer name