From d437d223d17a102bc6ae20264676797e4736c024 Mon Sep 17 00:00:00 2001 From: HBinmore <102923867+HBinmore@users.noreply.github.com> Date: Wed, 18 May 2022 18:21:09 +0100 Subject: [PATCH 1/4] Initial customer setup - Db changes to add customer table (INCLUDES MIGRATION) - Basic controller methods to get single, get all, and create customer --- DeveloperTest/Business/CustomerService.cs | 73 ++++++++++++++++++ .../Business/Interfaces/ICustomerService.cs | 11 +++ .../Controllers/CustomerController.cs | 49 ++++++++++++ .../Database/ApplicationDbContext.cs | 11 +++ DeveloperTest/Database/Models/Customer.cs | 12 +++ DeveloperTest/Enums/CustomerType.cs | 8 ++ .../20220518164245_CustomerTable.Designer.cs | 75 +++++++++++++++++++ .../20220518164245_CustomerTable.cs | 32 ++++++++ .../ApplicationDbContextModelSnapshot.cs | 21 +++++- DeveloperTest/Models/BaseCustomerModel.cs | 13 ++++ DeveloperTest/Models/CustomerModel.cs | 14 ++++ DeveloperTest/Startup.cs | 1 + 12 files changed, 319 insertions(+), 1 deletion(-) 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/Enums/CustomerType.cs create mode 100644 DeveloperTest/Migrations/20220518164245_CustomerTable.Designer.cs create mode 100644 DeveloperTest/Migrations/20220518164245_CustomerTable.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..1e04483 --- /dev/null +++ b/DeveloperTest/Business/CustomerService.cs @@ -0,0 +1,73 @@ +using DeveloperTest.Business.Interfaces; +using DeveloperTest.Database; +using DeveloperTest.Database.Models; +using DeveloperTest.Enums; +using DeveloperTest.Models; +using System.Linq; + +namespace DeveloperTest.Business +{ + public class CustomerService : ICustomerService + { + private readonly ApplicationDbContext context; + + public CustomerService(ApplicationDbContext context) + { + //Set db context + this.context = context; + } + + /// + /// Create a new customer, and return with Id number. + /// + /// Customer to be created. + /// + public CustomerModel CreateCustomer(BaseCustomerModel model) + { + var addedCustomer = context.Customers.Add(new Customer + { + CustomerType = model.CustomerType, + Name = model.Name + }); + + context.SaveChanges(); + + return new CustomerModel + { + CustomerId = addedCustomer.Entity.CustomerId, + CustomerType = addedCustomer.Entity.CustomerType, + Name = addedCustomer.Entity.Name + }; + } + + /// + /// Get a SINGLE customer + /// + /// Customer Id + /// + public CustomerModel GetCustomer(int id) + { + //Will return null if no customer found for provided Id. + return context.Customers.Where(x => x.CustomerId == id).Select(x => new CustomerModel + { + CustomerId = x.CustomerId, + CustomerType = x.CustomerType, + Name = x.Name + }).SingleOrDefault(); + } + + /// + /// Get ALL customers + /// + /// + public CustomerModel[] GetCustomers() + { + return context.Customers.Select(x => new CustomerModel + { + CustomerId = x.CustomerId, + CustomerType = x.CustomerType, + Name = x.Name + }).ToArray(); + } + } +} diff --git a/DeveloperTest/Business/Interfaces/ICustomerService.cs b/DeveloperTest/Business/Interfaces/ICustomerService.cs new file mode 100644 index 0000000..54f1f38 --- /dev/null +++ b/DeveloperTest/Business/Interfaces/ICustomerService.cs @@ -0,0 +1,11 @@ +using DeveloperTest.Models; + +namespace DeveloperTest.Business.Interfaces +{ + public interface ICustomerService + { + CustomerModel[] GetCustomers(); + CustomerModel GetCustomer(int id); + CustomerModel CreateCustomer(BaseCustomerModel model); + } +} diff --git a/DeveloperTest/Controllers/CustomerController.cs b/DeveloperTest/Controllers/CustomerController.cs new file mode 100644 index 0000000..ab06c4d --- /dev/null +++ b/DeveloperTest/Controllers/CustomerController.cs @@ -0,0 +1,49 @@ +using DeveloperTest.Business.Interfaces; +using DeveloperTest.Models; +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Mvc; + +namespace DeveloperTest.Controllers +{ + [ApiController, Route("[controller]")] + public class CustomerController : ControllerBase + { + private readonly ICustomerService customerService; + + public CustomerController(ICustomerService customerService) + { + this.customerService = customerService; + } + + [HttpGet] + public IActionResult Get() + { + return Ok(customerService.GetCustomers()); + } + + [HttpGet("{id}")] + public IActionResult Get(int id) + { + var customer = customerService.GetCustomer(id); + + if (customer == null) + { + return NotFound(); + } + + return Ok(customer); + } + + [HttpPost] + public IActionResult Create(BaseCustomerModel model) + { + if (!ModelState.IsValid) + { + return BadRequest(ModelState); + } + + var customer = customerService.CreateCustomer(model); + return Created($"customer/{customer.CustomerId}", customer); + } + } +} diff --git a/DeveloperTest/Database/ApplicationDbContext.cs b/DeveloperTest/Database/ApplicationDbContext.cs index f5be4a1..4b942e1 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) { @@ -17,6 +18,7 @@ protected override void OnModelCreating(ModelBuilder modelBuilder) { base.OnModelCreating(modelBuilder); + //Job modelBuilder.Entity() .HasKey(x => x.JobId); @@ -31,6 +33,15 @@ protected override void OnModelCreating(ModelBuilder modelBuilder) Engineer = "Test", When = new DateTime(2022, 2, 1, 12, 0, 0) }); + + //Customer + modelBuilder.Entity() + .HasKey(x => x.CustomerId); + + modelBuilder.Entity() + .Property(x => x.CustomerId) + .ValueGeneratedOnAdd(); + } } } diff --git a/DeveloperTest/Database/Models/Customer.cs b/DeveloperTest/Database/Models/Customer.cs new file mode 100644 index 0000000..fd59466 --- /dev/null +++ b/DeveloperTest/Database/Models/Customer.cs @@ -0,0 +1,12 @@ +using DeveloperTest.Enums; + +namespace DeveloperTest.Database.Models +{ + public class Customer + { + public int CustomerId { get; set; } + public string Name { get; set; } + public CustomerType CustomerType { get; set; } + } + +} diff --git a/DeveloperTest/Enums/CustomerType.cs b/DeveloperTest/Enums/CustomerType.cs new file mode 100644 index 0000000..fdc1045 --- /dev/null +++ b/DeveloperTest/Enums/CustomerType.cs @@ -0,0 +1,8 @@ +namespace DeveloperTest.Enums +{ + public enum CustomerType + { + Large, + Small + } +} diff --git a/DeveloperTest/Migrations/20220518164245_CustomerTable.Designer.cs b/DeveloperTest/Migrations/20220518164245_CustomerTable.Designer.cs new file mode 100644 index 0000000..a56c440 --- /dev/null +++ b/DeveloperTest/Migrations/20220518164245_CustomerTable.Designer.cs @@ -0,0 +1,75 @@ +// +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("20220518164245_CustomerTable")] + partial class CustomerTable + { + 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("CustomerType") + .HasColumnType("int"); + + b.Property("Name") + .HasColumnType("nvarchar(max)"); + + b.HasKey("CustomerId"); + + b.ToTable("Customers"); + }); + + 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/20220518164245_CustomerTable.cs b/DeveloperTest/Migrations/20220518164245_CustomerTable.cs new file mode 100644 index 0000000..8059993 --- /dev/null +++ b/DeveloperTest/Migrations/20220518164245_CustomerTable.cs @@ -0,0 +1,32 @@ +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace DeveloperTest.Migrations +{ + public partial class CustomerTable : 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), + CustomerType = table.Column(type: "int", nullable: false) + }, + constraints: table => + { + table.PrimaryKey("PK_Customers", x => x.CustomerId); + }); + } + + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropTable( + name: "Customers"); + } + } +} diff --git a/DeveloperTest/Migrations/ApplicationDbContextModelSnapshot.cs b/DeveloperTest/Migrations/ApplicationDbContextModelSnapshot.cs index 0ee623b..48f3f8c 100644 --- a/DeveloperTest/Migrations/ApplicationDbContextModelSnapshot.cs +++ b/DeveloperTest/Migrations/ApplicationDbContextModelSnapshot.cs @@ -22,6 +22,25 @@ 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("CustomerType") + .HasColumnType("int"); + + b.Property("Name") + .HasColumnType("nvarchar(max)"); + + b.HasKey("CustomerId"); + + b.ToTable("Customers", (string)null); + }); + modelBuilder.Entity("DeveloperTest.Database.Models.Job", b => { b.Property("JobId") @@ -38,7 +57,7 @@ protected override void BuildModel(ModelBuilder modelBuilder) b.HasKey("JobId"); - b.ToTable("Jobs"); + b.ToTable("Jobs", (string)null); b.HasData( new diff --git a/DeveloperTest/Models/BaseCustomerModel.cs b/DeveloperTest/Models/BaseCustomerModel.cs new file mode 100644 index 0000000..ee75d3e --- /dev/null +++ b/DeveloperTest/Models/BaseCustomerModel.cs @@ -0,0 +1,13 @@ +using DeveloperTest.Enums; +using System.ComponentModel.DataAnnotations; + +namespace DeveloperTest.Models +{ + public class BaseCustomerModel + { + [Required, MinLength(5, ErrorMessage = "Name must be at least 5 characters in length.")] + public string Name { get; set; } + [EnumDataType(typeof(CustomerType), ErrorMessage = "Invalid enum value.")] + public CustomerType CustomerType { get; set; } + } +} diff --git a/DeveloperTest/Models/CustomerModel.cs b/DeveloperTest/Models/CustomerModel.cs new file mode 100644 index 0000000..39b5591 --- /dev/null +++ b/DeveloperTest/Models/CustomerModel.cs @@ -0,0 +1,14 @@ +using DeveloperTest.Enums; +using System.ComponentModel.DataAnnotations; + +namespace DeveloperTest.Models +{ + public class CustomerModel + { + public int CustomerId { get; set; } + [Required, MinLength(5)] + public string Name { get; set; } + [Required] + public CustomerType CustomerType { 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 51c15220df775bd297281666ee7b0c88d4649847 Mon Sep 17 00:00:00 2001 From: HBinmore <102923867+HBinmore@users.noreply.github.com> Date: Wed, 18 May 2022 19:39:37 +0100 Subject: [PATCH 2/4] Customer ui - Listing & viewing customers - Creating customer --- ui/src/app/app-routing.module.ts | 7 +++- 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 | 30 ++++++++++++++ ui/src/app/customer/customer.component.html | 29 +++++++++++++ ui/src/app/customer/customer.component.scss | 40 ++++++++++++++++++ .../app/customer/customer.component.spec.ts | 25 +++++++++++ ui/src/app/customer/customer.component.ts | 41 +++++++++++++++++++ ui/src/app/models/customer.model.ts | 10 +++++ ui/src/app/services/customer.service.spec.ts | 12 ++++++ ui/src/app/services/customer.service.ts | 25 +++++++++++ 14 files changed, 254 insertions(+), 2 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..ade320e 100644 --- a/ui/src/app/app-routing.module.ts +++ b/ui/src/app/app-routing.module.ts @@ -3,13 +3,18 @@ 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..d971ad2 --- /dev/null +++ b/ui/src/app/customer-detail/customer-detail.component.html @@ -0,0 +1,5 @@ +

Customer Id: {{customer.customerId}}

+

Name: {{customer.name}}

+

Type: {{CustomerType[customer.customerType]}}

+ +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..94af8fe --- /dev/null +++ b/ui/src/app/customer-detail/customer-detail.component.ts @@ -0,0 +1,30 @@ +import { Component, OnInit } from '@angular/core'; +import { ActivatedRoute } from '@angular/router'; +import { CustomerModel, CustomerType } 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 { + + private customerId: number; + + public customer: CustomerModel; + CustomerType = CustomerType; + + + constructor( + private route: ActivatedRoute, + private customerService: CustomerService) { + this.customerId = route.snapshot.params.id; + } + + ngOnInit() { + 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..810e2c4 --- /dev/null +++ b/ui/src/app/customer/customer.component.html @@ -0,0 +1,29 @@ +

New customer form

+
+ + + + +
+ +

Customers list

+ + + + + + + + + + + + + + + +
NameType
{{customer.name}}{{CustomerType[customer.customerType]}} + 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..9b45842 --- /dev/null +++ b/ui/src/app/customer/customer.component.scss @@ -0,0 +1,40 @@ +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; + } + } \ No newline at end of file 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..844809a --- /dev/null +++ b/ui/src/app/customer/customer.component.ts @@ -0,0 +1,41 @@ +import { Component, OnInit } from '@angular/core'; +import { NgForm } from '@angular/forms'; +import { CustomerModel, CustomerType } 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 customers: CustomerModel[] = []; + CustomerType = CustomerType; + customerTypes: string[] = []; + + public newCustomer: CustomerModel = { + customerId: null, + name: null, + customerType: null + }; + + constructor(private customerService: CustomerService ) { + this.customerTypes = ["Large", "Small"]; //hackwork - should really be pulling from the enum directly for future proofing. + } + + ngOnInit(): void { + this.customerService.GetCustomers().subscribe(customers => this.customers = customers); + } + + public createCustomer(form: NgForm): void { + if (form.invalid) { + alert('form is not valid'); + } else { + this.customerService.CreateCustomer(this.newCustomer).then(() => { + this.customerService.GetCustomers().subscribe(customers => this.customers = customers); + }); + } + } + +} diff --git a/ui/src/app/models/customer.model.ts b/ui/src/app/models/customer.model.ts new file mode 100644 index 0000000..aa5c53b --- /dev/null +++ b/ui/src/app/models/customer.model.ts @@ -0,0 +1,10 @@ +export interface CustomerModel { + customerId: number; + name: string; + customerType: CustomerType; + } + + export enum CustomerType{ + Large = 0, + Small = 1 + } \ No newline at end of file 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..deaab94 --- /dev/null +++ b/ui/src/app/services/customer.service.spec.ts @@ -0,0 +1,12 @@ +import { TestBed } from '@angular/core/testing'; + +import { CustomerService } from './customer.service'; + +describe('CustomerService', () => { + beforeEach(() => TestBed.configureTestingModule({})); + + it('should be created', () => { + const service: CustomerService = TestBed.get(CustomerService); + expect(service).toBeTruthy(); + }); +}); \ No newline at end of file diff --git a/ui/src/app/services/customer.service.ts b/ui/src/app/services/customer.service.ts new file mode 100644 index 0000000..b9165af --- /dev/null +++ b/ui/src/app/services/customer.service.ts @@ -0,0 +1,25 @@ +import { Injectable } from '@angular/core'; +import { HttpClient } from '@angular/common/http'; +import { Observable } from 'rxjs'; +import { CustomerModel } from '../models/customer.model'; + +@Injectable({ + providedIn: 'root' +}) +export class CustomerService { + + constructor(private httpClient: HttpClient) { } + + public GetCustomers(): Observable { + return this.httpClient.get('http://localhost:63235/customer'); + } + + public GetCustomer(customerId: number): Observable{ + return this.httpClient.get(`http://localhost:63235/customer/${customerId}`); + } + + public CreateCustomer(newCustomer: CustomerModel) : Promise { + return this.httpClient.post('http://localhost:63235/customer', newCustomer).toPromise(); + } + +} From c98cb53e0d1694cdd9746ba991f9c502fe1c8281 Mon Sep 17 00:00:00 2001 From: HBinmore <102923867+HBinmore@users.noreply.github.com> Date: Wed, 18 May 2022 20:04:16 +0100 Subject: [PATCH 3/4] Add customer to job - Nullable customer added to job - Get job(s) methods expanded to return customer if present --- DeveloperTest/Business/JobService.cs | 40 ++++++--- DeveloperTest/Controllers/JobController.cs | 5 ++ .../Database/ApplicationDbContext.cs | 1 + DeveloperTest/Database/Models/Job.cs | 4 + .../20220518184222_JobCustomer.Designer.cs | 89 +++++++++++++++++++ .../Migrations/20220518184222_JobCustomer.cs | 45 ++++++++++ .../ApplicationDbContextModelSnapshot.cs | 18 +++- DeveloperTest/Models/BaseJobModel.cs | 3 + DeveloperTest/Models/JobModel.cs | 3 + 9 files changed, 192 insertions(+), 16 deletions(-) create mode 100644 DeveloperTest/Migrations/20220518184222_JobCustomer.Designer.cs create mode 100644 DeveloperTest/Migrations/20220518184222_JobCustomer.cs diff --git a/DeveloperTest/Business/JobService.cs b/DeveloperTest/Business/JobService.cs index 7eb8f64..023df36 100644 --- a/DeveloperTest/Business/JobService.cs +++ b/DeveloperTest/Business/JobService.cs @@ -3,6 +3,7 @@ using DeveloperTest.Database; using DeveloperTest.Database.Models; using DeveloperTest.Models; +using Microsoft.EntityFrameworkCore; namespace DeveloperTest.Business { @@ -17,22 +18,31 @@ public JobService(ApplicationDbContext context) public JobModel[] GetJobs() { - return context.Jobs.Select(x => new JobModel - { - JobId = x.JobId, - Engineer = x.Engineer, - When = x.When - }).ToArray(); + return context.Jobs + .Include(x => x.Customer) + .Select(x => new JobModel + { + JobId = x.JobId, + Engineer = x.Engineer, + When = x.When, + CustomerId = x.CustomerId, + Customer = x.Customer != null ? new CustomerModel() { CustomerId = x.Customer.CustomerId, CustomerType = x.Customer.CustomerType, Name = x.Customer.Name } : null + + }).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(x => x.Customer) + .Where(x => x.JobId == jobId).Select(x => new JobModel + { + JobId = x.JobId, + Engineer = x.Engineer, + When = x.When, + CustomerId = x.CustomerId, + Customer = x.Customer != null ? new CustomerModel() { CustomerId = x.Customer.CustomerId, CustomerType = x.Customer.CustomerType, Name = x.Customer.Name } : null + }).SingleOrDefault(); } public JobModel CreateJob(BaseJobModel model) @@ -40,7 +50,8 @@ public JobModel CreateJob(BaseJobModel model) var addedJob = context.Jobs.Add(new Job { Engineer = model.Engineer, - When = model.When + When = model.When, + CustomerId = model.CustomerId }); context.SaveChanges(); @@ -49,7 +60,8 @@ public JobModel CreateJob(BaseJobModel model) { JobId = addedJob.Entity.JobId, Engineer = addedJob.Entity.Engineer, - When = addedJob.Entity.When + When = addedJob.Entity.When, + CustomerId = addedJob.Entity.CustomerId }; } } diff --git a/DeveloperTest/Controllers/JobController.cs b/DeveloperTest/Controllers/JobController.cs index 2ce1c0e..7a8d2e5 100644 --- a/DeveloperTest/Controllers/JobController.cs +++ b/DeveloperTest/Controllers/JobController.cs @@ -37,6 +37,11 @@ public IActionResult Get(int id) [HttpPost] public IActionResult Create(BaseJobModel model) { + if (!ModelState.IsValid) + { + return BadRequest(ModelState); + } + if (model.When.Date < DateTime.Now.Date) { return BadRequest("Date cannot be in the past"); diff --git a/DeveloperTest/Database/ApplicationDbContext.cs b/DeveloperTest/Database/ApplicationDbContext.cs index 4b942e1..8916cd5 100644 --- a/DeveloperTest/Database/ApplicationDbContext.cs +++ b/DeveloperTest/Database/ApplicationDbContext.cs @@ -34,6 +34,7 @@ protected override void OnModelCreating(ModelBuilder modelBuilder) When = new DateTime(2022, 2, 1, 12, 0, 0) }); + //Customer modelBuilder.Entity() .HasKey(x => x.CustomerId); diff --git a/DeveloperTest/Database/Models/Job.cs b/DeveloperTest/Database/Models/Job.cs index 8a2abd0..02bb13e 100644 --- a/DeveloperTest/Database/Models/Job.cs +++ b/DeveloperTest/Database/Models/Job.cs @@ -9,5 +9,9 @@ public class Job public string Engineer { get; set; } public DateTime When { get; set; } + + //Nullable customer - jobs should be created with one, but existing jobs shouldn't crash out if they don't have one. + public int? CustomerId { get; set; } + public Customer Customer { get; set; } } } diff --git a/DeveloperTest/Migrations/20220518184222_JobCustomer.Designer.cs b/DeveloperTest/Migrations/20220518184222_JobCustomer.Designer.cs new file mode 100644 index 0000000..9fb09d5 --- /dev/null +++ b/DeveloperTest/Migrations/20220518184222_JobCustomer.Designer.cs @@ -0,0 +1,89 @@ +// +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("20220518184222_JobCustomer")] + partial class JobCustomer + { + 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("CustomerType") + .HasColumnType("int"); + + b.Property("Name") + .HasColumnType("nvarchar(max)"); + + b.HasKey("CustomerId"); + + b.ToTable("Customers"); + }); + + 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() + .HasForeignKey("CustomerId"); + + b.Navigation("Customer"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/DeveloperTest/Migrations/20220518184222_JobCustomer.cs b/DeveloperTest/Migrations/20220518184222_JobCustomer.cs new file mode 100644 index 0000000..5d0cf5f --- /dev/null +++ b/DeveloperTest/Migrations/20220518184222_JobCustomer.cs @@ -0,0 +1,45 @@ +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace DeveloperTest.Migrations +{ + public partial class JobCustomer : 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 48f3f8c..d1c0503 100644 --- a/DeveloperTest/Migrations/ApplicationDbContextModelSnapshot.cs +++ b/DeveloperTest/Migrations/ApplicationDbContextModelSnapshot.cs @@ -38,7 +38,7 @@ protected override void BuildModel(ModelBuilder modelBuilder) b.HasKey("CustomerId"); - b.ToTable("Customers", (string)null); + b.ToTable("Customers"); }); modelBuilder.Entity("DeveloperTest.Database.Models.Job", b => @@ -49,6 +49,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)"); @@ -57,7 +60,9 @@ protected override void BuildModel(ModelBuilder modelBuilder) b.HasKey("JobId"); - b.ToTable("Jobs", (string)null); + b.HasIndex("CustomerId"); + + b.ToTable("Jobs"); b.HasData( new @@ -67,6 +72,15 @@ 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() + .HasForeignKey("CustomerId"); + + b.Navigation("Customer"); + }); #pragma warning restore 612, 618 } } diff --git a/DeveloperTest/Models/BaseJobModel.cs b/DeveloperTest/Models/BaseJobModel.cs index d2bc052..884d3a0 100644 --- a/DeveloperTest/Models/BaseJobModel.cs +++ b/DeveloperTest/Models/BaseJobModel.cs @@ -1,4 +1,5 @@ using System; +using System.ComponentModel.DataAnnotations; namespace DeveloperTest.Models { @@ -7,5 +8,7 @@ public class BaseJobModel public string Engineer { get; set; } public DateTime When { get; set; } + [Required(ErrorMessage = "A customer id is required to create a new job.")] + public int? CustomerId { get; set; } } } diff --git a/DeveloperTest/Models/JobModel.cs b/DeveloperTest/Models/JobModel.cs index 8f2b5be..7442602 100644 --- a/DeveloperTest/Models/JobModel.cs +++ b/DeveloperTest/Models/JobModel.cs @@ -9,5 +9,8 @@ public class JobModel public string Engineer { get; set; } public DateTime When { get; set; } + + public int? CustomerId { get; set; } + public CustomerModel Customer { get; set; } } } From 8adb0149f33b0f5f2c9b821c8da8573cf87d2143 Mon Sep 17 00:00:00 2001 From: HBinmore <102923867+HBinmore@users.noreply.github.com> Date: Wed, 18 May 2022 20:19:02 +0100 Subject: [PATCH 4/4] Selecting customer on new job - Added the ability to select a customer when creating a new job --- ui/src/app/customer/customer.component.html | 4 ++++ ui/src/app/job-detail/job-detail.component.html | 2 ++ ui/src/app/job-detail/job-detail.component.ts | 2 ++ ui/src/app/job/job.component.html | 8 ++++++++ ui/src/app/job/job.component.ts | 13 +++++++++++-- ui/src/app/models/job.model.ts | 4 ++++ 6 files changed, 31 insertions(+), 2 deletions(-) diff --git a/ui/src/app/customer/customer.component.html b/ui/src/app/customer/customer.component.html index 810e2c4..7c15a0c 100644 --- a/ui/src/app/customer/customer.component.html +++ b/ui/src/app/customer/customer.component.html @@ -2,9 +2,13 @@

New customer form

+ Please enter a customer name (at least 5 characters) + + Please select a customer type
diff --git a/ui/src/app/job-detail/job-detail.component.html b/ui/src/app/job-detail/job-detail.component.html index b92e031..02d4d22 100644 --- a/ui/src/app/job-detail/job-detail.component.html +++ b/ui/src/app/job-detail/job-detail.component.html @@ -1,4 +1,6 @@

JobId: {{job.jobId}}

+

Customer: {{job.customer?.name ?? "Unknown"}}

+

Customer Type: {{job.customer?.customerType !== null ? CustomerType[job.customer.customerType] : "Unknown" }}

Engineer: {{job.engineer}}

When: {{job.when | date:'shortDate'}}

diff --git a/ui/src/app/job-detail/job-detail.component.ts b/ui/src/app/job-detail/job-detail.component.ts index 0ce610d..134cc2c 100644 --- a/ui/src/app/job-detail/job-detail.component.ts +++ b/ui/src/app/job-detail/job-detail.component.ts @@ -2,6 +2,7 @@ import { Component, OnInit } from '@angular/core'; import { ActivatedRoute } from '@angular/router'; import { JobService } from '../services/job.service'; import { JobModel } from '../models/job.model'; +import { CustomerType } from '../models/customer.model'; @Component({ selector: 'app-job-detail', @@ -13,6 +14,7 @@ export class JobDetailComponent implements OnInit { private jobId: number; public job: JobModel; + CustomerType = CustomerType; constructor( private route: ActivatedRoute, diff --git a/ui/src/app/job/job.component.html b/ui/src/app/job/job.component.html index 085c531..3fcb24e 100644 --- a/ui/src/app/job/job.component.html +++ b/ui/src/app/job/job.component.html @@ -6,6 +6,12 @@

New job form

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

Jobs list

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

Jobs list

{{job.engineer}} + {{job.customer?.name ?? "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..454e2b8 100644 --- a/ui/src/app/job/job.component.ts +++ b/ui/src/app/job/job.component.ts @@ -3,6 +3,8 @@ import { NgForm } from '@angular/forms'; import { EngineerService } from '../services/engineer.service'; import { JobService } from '../services/job.service'; import { JobModel } from '../models/job.model'; +import { CustomerService } from '../services/customer.service'; +import { CustomerModel } from '../models/customer.model'; @Component({ selector: 'app-job', @@ -15,19 +17,26 @@ export class JobComponent implements OnInit { public jobs: JobModel[] = []; + public customers: CustomerModel[] = []; + public newJob: JobModel = { jobId: null, engineer: null, - when: null + when: null, + customerId: null, + customer: null }; constructor( private engineerService: EngineerService, - private jobService: JobService) { } + private jobService: JobService, + private customerService: CustomerService) { } ngOnInit() { this.engineerService.GetEngineers().subscribe(engineers => this.engineers = engineers); this.jobService.GetJobs().subscribe(jobs => this.jobs = jobs); + //Get our customers for the job creation form + this.customerService.GetCustomers().subscribe(customers => this.customers = customers); } public createJob(form: NgForm): void { diff --git a/ui/src/app/models/job.model.ts b/ui/src/app/models/job.model.ts index 5c3342c..2cc1f80 100644 --- a/ui/src/app/models/job.model.ts +++ b/ui/src/app/models/job.model.ts @@ -1,5 +1,9 @@ +import { CustomerModel } from "./customer.model"; + export interface JobModel { jobId: number; engineer: string; when: Date; + customerId: number; + customer: CustomerModel; }