diff --git a/src/app/app-routing.module.ts b/src/app/app-routing.module.ts index 46c9583..3588363 100644 --- a/src/app/app-routing.module.ts +++ b/src/app/app-routing.module.ts @@ -11,11 +11,15 @@ import { UserDeleteComponent } from './user/user-delete/user-delete.component'; import { ShelterListComponent } from './shelter/shelter-list/shelter-list.component' import {ShelterCreateComponent} from "./shelter/shelter-create/shelter-create.component"; import { CatListComponent } from './pet/cat/cat-list/cat-list.component'; -import { DogListComponent } from './pet/dog/dog-list/dog-list.component'; import { CatCreateComponent } from './pet/cat/cat-create/cat-create.component'; import { CatDetailComponent } from './pet/cat/cat-detail/cat-detail.component'; import { CatEditComponent } from './pet/cat/cat-edit/cat-edit.component'; import { CatDeleteComponent } from './pet/cat/cat-delete/cat-delete.component'; +import { DogListComponent } from './pet/dog/dog-list/dog-list.component'; +import { DogCreateComponent } from './pet/dog/dog-create/dog-create.component'; +import { DogDetailComponent } from './pet/dog/dog-detail/dog-detail.component'; +import { DogEditComponent } from './pet/dog/dog-edit/dog-edit.component'; +import { DogDeleteComponent } from './pet/dog/dog-delete/dog-delete.component'; const routes: Routes = [ { path: 'users/create', component: UserRegisterComponent}, @@ -34,6 +38,10 @@ const routes: Routes = [ { path: 'cats/:id/edit', component: CatEditComponent, canActivate: [LoggedInGuard]}, { path: 'cats/:id/delete', component: CatDeleteComponent, canActivate: [LoggedInGuard]}, { path: 'dogs', component: DogListComponent, canActivate: [LoggedInGuard]}, + { path: 'dogs/create', component: DogCreateComponent, canActivate: [LoggedInGuard]}, + { path: 'dogs/:id', component: DogDetailComponent, canActivate: [LoggedInGuard]}, + { path: 'dogs/:id/edit', component: DogEditComponent, canActivate: [LoggedInGuard]}, + { path: 'dogs/:id/delete', component: DogDeleteComponent, canActivate: [LoggedInGuard]}, ]; @NgModule({ diff --git a/src/app/app.module.ts b/src/app/app.module.ts index 6a37913..f193570 100644 --- a/src/app/app.module.ts +++ b/src/app/app.module.ts @@ -29,7 +29,6 @@ import {ShelterListComponent} from "./shelter/shelter-list/shelter-list.componen import {ShelterCreateComponent} from "./shelter/shelter-create/shelter-create.component"; import {ShelterDetailComponent} from "./shelter/shelter-detail/shelter-detail.component"; import { CatListComponent } from './pet/cat/cat-list/cat-list.component'; -import { DogListComponent } from './pet/dog/dog-list/dog-list.component'; import { CatService } from './pet/cat/cat.service'; import { DogService } from './pet/dog/dog.service'; import { CatCreateComponent } from './pet/cat/cat-create/cat-create.component'; @@ -37,6 +36,12 @@ import { CatDetailComponent } from './pet/cat/cat-detail/cat-detail.component'; import { CatEditComponent } from './pet/cat/cat-edit/cat-edit.component'; import { CatDeleteComponent } from './pet/cat/cat-delete/cat-delete.component'; import { CatSearchComponent } from './pet/cat/cat-search/cat-search.component'; +import { DogListComponent } from './pet/dog/dog-list/dog-list.component'; +import { DogCreateComponent } from './pet/dog/dog-create/dog-create.component'; +import { DogDetailComponent } from './pet/dog/dog-detail/dog-detail.component'; +import { DogDeleteComponent } from './pet/dog/dog-delete/dog-delete.component'; +import { DogEditComponent } from './pet/dog/dog-edit/dog-edit.component'; +import { DogSearchComponent } from './pet/dog/dog-search/dog-search.component'; @NgModule({ declarations: [ @@ -60,6 +65,11 @@ import { CatSearchComponent } from './pet/cat/cat-search/cat-search.component'; CatEditComponent, CatSearchComponent, DogListComponent, + DogCreateComponent, + DogDetailComponent, + DogDeleteComponent, + DogEditComponent, + DogSearchComponent, ], imports: [ BrowserModule, diff --git a/src/app/pet/dog/dog-create/dog-create.component.css b/src/app/pet/dog/dog-create/dog-create.component.css new file mode 100644 index 0000000..72f16bc --- /dev/null +++ b/src/app/pet/dog/dog-create/dog-create.component.css @@ -0,0 +1,4 @@ +.form-error { + color: red; + font-size: x-small; +} \ No newline at end of file diff --git a/src/app/pet/dog/dog-create/dog-create.component.html b/src/app/pet/dog/dog-create/dog-create.component.html new file mode 100644 index 0000000..1d290a6 --- /dev/null +++ b/src/app/pet/dog/dog-create/dog-create.component.html @@ -0,0 +1,198 @@ +
+
+
+
+ + +
+ Name is required +
+
+ +
+ + +
+ Chip is required +
+
+ +
+ + +
+ Date of birth is required +
+
+ Invalid date +
+
+ +
+ + +
+ Colour is required +
+
+ Invalid date +
+
+ +
+ + +
+ Size is required +
+
+ Size is invalid +
+
+ +
+ + +
+ Sex is required +
+
+ Sex is invalid +
+
+ +
+ + +
+ Race is required +
+
+ +
+ + +
+ +
+ + +
+ Barking level is required +
+
+ +
+ + +
+ + +
+
+
diff --git a/src/app/pet/dog/dog-create/dog-create.component.ts b/src/app/pet/dog/dog-create/dog-create.component.ts new file mode 100644 index 0000000..a6fdbf0 --- /dev/null +++ b/src/app/pet/dog/dog-create/dog-create.component.ts @@ -0,0 +1,156 @@ +import {Component, OnInit} from '@angular/core'; +import {Router} from "@angular/router"; +import {DogService} from "../dog.service"; +import {Dog} from "../dog"; +import {AbstractControl, FormControl, FormGroup, ValidationErrors, ValidatorFn, Validators} from "@angular/forms"; +import {PagedResourceCollection} from "@lagoshny/ngx-hateoas-client"; + +@Component({ + selector: 'app-dog-create', + templateUrl: './dog-create.component.html', + styleUrls: ['./dog-create.component.css'] +}) +export class DogCreateComponent implements OnInit{ + + closeResult = ''; + public isModalSaved: boolean = false; + public dogs: Dog[] = []; + public dog: Dog; + public dogForm: FormGroup; + + constructor( + private router: Router, + private dogService: DogService, + ) {} + + ngOnInit(): void { + this.dog = new Dog(); + this.dogForm = new FormGroup({ + chip: new FormControl(this.dog.chip, [ + Validators.required, + ]), + name: new FormControl(this.dog.name, [ + Validators.required, + ]), + dateOfBirth: new FormControl(this.dog.dateOfBirth, [ + Validators.required, + this.dateValidator(), + ]), + colour: new FormControl(this.dog.colour, [ + Validators.required, + this.colourValidator(), + ]), + size: new FormControl(this.dog.size, [ + Validators.required, + this.numberValidator(), + ]), + sex: new FormControl(this.dog.sex, [ + Validators.required, + this.sexValidator(), + ]), + race: new FormControl(this.dog.race, [ + Validators.required, + ]), + dangerous: new FormControl(this.dog.dangerous, []), + barkingLevel: new FormControl(this.dog.barkingLevel, [ + Validators.required, + ]), + }); + this.loadDogList(); + } + + loadDogList() { + this.dogService + .getPage({ + sort: { name: 'ASC' }, + }) + .subscribe((dogs: PagedResourceCollection) => { + this.dogs = dogs.resources.sort((a, b) => + a.chip.localeCompare(b.chip) + ); + }); + } + + dateValidator(): ValidatorFn { + return (control: AbstractControl): ValidationErrors | null => { + const dateValue = control.value; + const isValidDate = !isNaN(Date.parse(dateValue)); + return isValidDate ? null : { invalidDate: { value: control.value } }; + }; + } + + colourValidator(): ValidatorFn { + const hexColorRe: RegExp = /^#([A-Fa-f0-9]{6}|[A-Fa-f0-9]{3})$/; + return (control: AbstractControl): ValidationErrors | null => { + const isValidColour = hexColorRe.test(control.value); + return isValidColour ? null : { invalidColour: { value: control.value } }; + }; + } + + numberValidator(): ValidatorFn { + return (control: AbstractControl): ValidationErrors | null => { + const numberValue = control.value; + const isValidNumber = !isNaN(numberValue) && numberValue > 0; + return isValidNumber ? null : { invalidNumber: { value: control.value } }; + }; + } + + sexValidator(): ValidatorFn { + const validSexes = ['male', 'female']; + return (control: AbstractControl): ValidationErrors | null => { + const isValidSex = control.value !== null && control.value !== undefined && validSexes.includes(control.value.toLowerCase()); + return isValidSex ? null : { invalidSex: { value: control.value } }; + }; + } + + get chip() { + return this.dogForm.get('chip'); + } + + get name() { + return this.dogForm.get('name'); + } + + get dateOfBirth() { + return this.dogForm.get('dateOfBirth'); + } + + get adopted() { + return this.dogForm.get('adopted'); + } + + get colour() { + return this.dogForm.get('colour'); + } + + get size() { + return this.dogForm.get('size'); + } + + get sex() { + return this.dogForm.get('sex'); + } + + get race() { + return this.dogForm.get('race'); + } + + get dangerous() { + return this.dogForm.get('dangerous'); + } + + get barkingLevel() { + return this.dogForm.get('barkingLevel'); + } + + onSubmit(): void { + this.dog.adopted = false; + this.dogService + .createResource({ body: this.dog }) + .subscribe((dog: Dog) => { + const uri = (dog as any).uri; + this.router.navigate([uri]).then(); + }); + } + +} diff --git a/src/app/pet/dog/dog-delete/dog-delete.component.html b/src/app/pet/dog/dog-delete/dog-delete.component.html new file mode 100644 index 0000000..ee8f0b6 --- /dev/null +++ b/src/app/pet/dog/dog-delete/dog-delete.component.html @@ -0,0 +1,16 @@ +
+
+
+

Are you sure you want to delete this record?

+
+
+
+
+ + +
+
+
+ \ No newline at end of file diff --git a/src/app/pet/dog/dog-delete/dog-delete.component.ts b/src/app/pet/dog/dog-delete/dog-delete.component.ts new file mode 100644 index 0000000..2417326 --- /dev/null +++ b/src/app/pet/dog/dog-delete/dog-delete.component.ts @@ -0,0 +1,31 @@ +import { Component, OnInit } from '@angular/core'; +import { ActivatedRoute, Router } from '@angular/router'; +import { Dog } from '../dog'; +import { DogService } from '../dog.service'; + +@Component({ + selector: 'app-dog-delete', + templateUrl: './dog-delete.component.html' +}) +export class DogDeleteComponent implements OnInit { + public dog: Dog = new Dog(); + private id: string; + + constructor(private route: ActivatedRoute, + private router: Router, + private dogService: DogService) { + } + + ngOnInit(): void { + this.id = this.route.snapshot.paramMap.get('id'); + this.dogService.getResource(this.id).subscribe( + dog => this.dog = dog); + } + + delete(): void { + this.dogService.deleteResource(this.dog).subscribe( + () => { + this.router.navigate(['dogs']); + }); + } +} diff --git a/src/app/pet/dog/dog-detail/dog-detail.component.html b/src/app/pet/dog/dog-detail/dog-detail.component.html new file mode 100644 index 0000000..393202d --- /dev/null +++ b/src/app/pet/dog/dog-detail/dog-detail.component.html @@ -0,0 +1,63 @@ +
+
+

{{dog.name}}

+
+
+
Chip
+

{{dog.chip}}

+
+
+
Date of birth
+

{{dog.dateOfBirth}}

+
+
+
Colour
+
+
+
+
Size
+

Small

+

Average

+

Large

+

Extra Large

+
+
+
Sex
+

Male

+

Female

+
+
+
Race
+

{{dog.race}}

+
+
+
Dangerous
+ + +
+
+
Adopted
+ + +
+
+
Barking level
+

No barking

+

Low barking

+

Average barking

+

Loud barking

+
+
+ +
+
\ No newline at end of file diff --git a/src/app/pet/dog/dog-detail/dog-detail.component.ts b/src/app/pet/dog/dog-detail/dog-detail.component.ts new file mode 100644 index 0000000..6e0bb88 --- /dev/null +++ b/src/app/pet/dog/dog-detail/dog-detail.component.ts @@ -0,0 +1,25 @@ +import { Component, OnInit } from '@angular/core'; +import { ActivatedRoute } from '@angular/router'; +import { DogService } from '../dog.service'; +import { Dog } from '../dog'; + +@Component({ + selector: 'app-dog-detail', + templateUrl: './dog-detail.component.html' +}) +export class DogDetailComponent implements OnInit { + public dog: Dog = new Dog(); + + constructor(private route: ActivatedRoute, + private dogService: DogService) { + } + + ngOnInit(): void { + const id = this.route.snapshot.paramMap.get('id'); + this.dogService.getResource(id).subscribe( + dog => { + this.dog = dog; + }); + } + +} diff --git a/src/app/pet/dog/dog-edit/dog-edit.component.html b/src/app/pet/dog/dog-edit/dog-edit.component.html new file mode 100644 index 0000000..7472239 --- /dev/null +++ b/src/app/pet/dog/dog-edit/dog-edit.component.html @@ -0,0 +1,198 @@ +
+
+
+
+ + +
+ Name is required +
+
+ +
+ + +
+ Chip is required +
+
+ +
+ + +
+ Date of birth is required +
+
+ Invalid date +
+
+ +
+ + +
+ Colour is required +
+
+ Invalid date +
+
+ +
+ + +
+ Size is required +
+
+ Size is invalid +
+
+ +
+ + +
+ Sex is required +
+
+ Sex is invalid +
+
+ +
+ + +
+ Race is required +
+
+ +
+ + +
+ +
+ + +
+ Barking level is required +
+
+ +
+ + +
+ + +
+
+
diff --git a/src/app/pet/dog/dog-edit/dog-edit.component.ts b/src/app/pet/dog/dog-edit/dog-edit.component.ts new file mode 100644 index 0000000..bec45db --- /dev/null +++ b/src/app/pet/dog/dog-edit/dog-edit.component.ts @@ -0,0 +1,139 @@ +import { Component, OnInit } from '@angular/core'; +import { ActivatedRoute } from '@angular/router'; +import { Router } from '@angular/router'; +import { Dog } from '../dog'; +import { DogService } from '../dog.service'; +import { AbstractControl, FormControl, FormGroup, ValidationErrors, ValidatorFn, Validators } from '@angular/forms'; + +@Component({ + selector: 'app-dog-edit', + templateUrl: './dog-edit.component.html' +}) +export class DogEditComponent implements OnInit { + public dog: Dog = new Dog(); + public dogForm: FormGroup; + + constructor(private route: ActivatedRoute, + private router: Router, + private dogService: DogService) { + } + + ngOnInit(): void { + const id = this.route.snapshot.paramMap.get('id'); + this.dogService.getResource(id).subscribe( + (dog: Dog) => this.dog = dog ); + this.dogForm = new FormGroup({ + chip: new FormControl(this.dog.chip, [ + Validators.required, + ]), + name: new FormControl(this.dog.name, [ + Validators.required, + ]), + dateOfBirth: new FormControl(this.dog.dateOfBirth, [ + Validators.required, + this.dateValidator(), + ]), + colour: new FormControl(this.dog.colour, [ + Validators.required, + this.colourValidator(), + ]), + size: new FormControl(this.dog.size, [ + Validators.required, + this.numberValidator(), + ]), + sex: new FormControl(this.dog.sex, [ + Validators.required, + this.sexValidator(), + ]), + race: new FormControl(this.dog.race, [ + Validators.required, + ]), + dangerous: new FormControl(this.dog.dangerous, []), + barkingLevel: new FormControl(this.dog.barkingLevel, [ + Validators.required, + ]), + }); + } + + + dateValidator(): ValidatorFn { + return (control: AbstractControl): ValidationErrors | null => { + const dateValue = control.value; + const isValidDate = !isNaN(Date.parse(dateValue)); + return isValidDate ? null : { invalidDate: { value: control.value } }; + }; + } + + colourValidator(): ValidatorFn { + const hexColorRe: RegExp = /^#([A-Fa-f0-9]{6}|[A-Fa-f0-9]{3})$/; + return (control: AbstractControl): ValidationErrors | null => { + const isValidColour = hexColorRe.test(control.value); + return isValidColour ? null : { invalidColour: { value: control.value } }; + }; + } + + numberValidator(): ValidatorFn { + return (control: AbstractControl): ValidationErrors | null => { + const numberValue = control.value; + const isValidNumber = !isNaN(numberValue) && numberValue > 0; + return isValidNumber ? null : { invalidNumber: { value: control.value } }; + }; + } + + sexValidator(): ValidatorFn { + const validSexes = ['male', 'female']; + return (control: AbstractControl): ValidationErrors | null => { + const isValidSex = control.value !== null && control.value !== undefined && validSexes.includes(control.value.toLowerCase()); + return isValidSex ? null : { invalidSex: { value: control.value } }; + }; + } + + get chip() { + return this.dogForm.get('chip'); + } + + get name() { + return this.dogForm.get('name'); + } + + get dateOfBirth() { + return this.dogForm.get('dateOfBirth'); + } + + get adopted() { + return this.dogForm.get('adopted'); + } + + get colour() { + return this.dogForm.get('colour'); + } + + get size() { + return this.dogForm.get('size'); + } + + get sex() { + return this.dogForm.get('sex'); + } + + get race() { + return this.dogForm.get('race'); + } + + get dangerous() { + return this.dogForm.get('dangerous'); + } + + get barkingLevel() { + return this.dogForm.get('barkingLevel'); + } + + + onSubmit(): void { + this.dogService.patchResource(this.dog).subscribe( + (patchedDog: Dog) => { + this.router.navigate(['dogs', patchedDog.id]); + }); + } + +} diff --git a/src/app/pet/dog/dog-list/dog-list.component.html b/src/app/pet/dog/dog-list/dog-list.component.html index c9d0a98..717a80a 100644 --- a/src/app/pet/dog/dog-list/dog-list.component.html +++ b/src/app/pet/dog/dog-list/dog-list.component.html @@ -1,14 +1,52 @@ +
+ +

Dogs

+
+
+ + +
-
-
Name
- {{dog.name}} -
-
+
Chip

{{dog.chip}}

+
+
Name
+

{{dog.name}}

+
+
+
Date of birth
+

{{dog.dateOfBirth}}

+
+
+ + +
diff --git a/src/app/pet/dog/dog-list/dog-list.component.ts b/src/app/pet/dog/dog-list/dog-list.component.ts index 8015c16..d1edc63 100644 --- a/src/app/pet/dog/dog-list/dog-list.component.ts +++ b/src/app/pet/dog/dog-list/dog-list.component.ts @@ -41,4 +41,9 @@ export class DogListComponent implements OnInit { (page: PagedResourceCollection) => (this.dogs = page.resources) ); } + + detail(dog: Dog): void { + this.router.navigate(['/dogs', dog.id]); + } + } diff --git a/src/app/pet/dog/dog-search/dog-search.component.html b/src/app/pet/dog/dog-search/dog-search.component.html new file mode 100644 index 0000000..910bb08 --- /dev/null +++ b/src/app/pet/dog/dog-search/dog-search.component.html @@ -0,0 +1,12 @@ +
+ +
Sorry, suggestions could not be loaded.
+
+ + +
+
{{r.name}} - {{r.chip}}
+
+ \ No newline at end of file diff --git a/src/app/pet/dog/dog-search/dog-search.component.ts b/src/app/pet/dog/dog-search/dog-search.component.ts new file mode 100644 index 0000000..3774ca8 --- /dev/null +++ b/src/app/pet/dog/dog-search/dog-search.component.ts @@ -0,0 +1,42 @@ +import { Component, EventEmitter, Output } from '@angular/core'; +import { Observable, of, OperatorFunction } from 'rxjs'; +import { catchError, debounceTime, distinctUntilChanged, map, switchMap, tap } from 'rxjs/operators'; +import { ResourceCollection } from '@lagoshny/ngx-hateoas-client'; +import { Dog } from '../dog'; +import { DogService } from '../dog.service'; + +@Component({ + selector: 'app-dog-search', + templateUrl: './dog-search.component.html' +}) + +export class DogSearchComponent { + @Output() emitResults: EventEmitter = new EventEmitter(); + searchFailed = false; + searching = false; + + constructor(private dogService: DogService) { + } + + autocomplete: OperatorFunction = (text$: Observable) => + text$.pipe( + debounceTime(500), + distinctUntilChanged(), + tap(() => this.searching = true), + switchMap(term => term.length < 3 ? of([]) : + this.dogService.findByIdContaining(term).pipe( + map((collection: ResourceCollection) => collection.resources), + tap(() => this.searchFailed = false), + catchError(() => { + this.searchFailed = true; + return of([]); + }) + ) + ), + tap(() => this.searching = false ) + ) + + select(item: any): void { + this.emitResults.emit(item as Dog); + } +} diff --git a/src/app/pet/dog/dog.service.ts b/src/app/pet/dog/dog.service.ts index d1d96cd..fbafecd 100644 --- a/src/app/pet/dog/dog.service.ts +++ b/src/app/pet/dog/dog.service.ts @@ -1,6 +1,8 @@ import { Injectable } from "@angular/core"; import { PetService } from "../pet.service"; import { Dog } from "./dog"; +import { Observable, forkJoin, map } from "rxjs"; +import { ResourceCollection } from "@lagoshny/ngx-hateoas-client"; @Injectable({providedIn: 'root'}) export class DogService extends PetService { @@ -9,4 +11,18 @@ export class DogService extends PetService { super(Dog); } + public findByIdContaining(query: string): Observable> { + let res1 = this.searchCollection('findByName', { params: { name: query } }); + let res2 = this.searchCollection('findByChip', { params: { chip: query } }); + return forkJoin([res1, res2]).pipe( + map(([result1, result2]) => { + const combinedResources = [...result1.resources, ...result2.resources]; + return { + ...result1, + resources: combinedResources, + } as ResourceCollection; + }) + );; + } + } \ No newline at end of file diff --git a/src/app/pet/dog/dog.ts b/src/app/pet/dog/dog.ts index 6d348b5..545e33a 100644 --- a/src/app/pet/dog/dog.ts +++ b/src/app/pet/dog/dog.ts @@ -4,6 +4,6 @@ import { Pet } from '../pet'; @HateoasResource('dogs') export class Dog extends Pet { - meowingLevel: number; + barkingLevel: number; } \ No newline at end of file