All owner endpoints finished
This commit is contained in:
parent
60dbcc3927
commit
d0c40d51cd
45 changed files with 968 additions and 28 deletions
|
@ -34,7 +34,8 @@
|
|||
"styles": [
|
||||
"src/styles.scss"
|
||||
],
|
||||
"scripts": []
|
||||
"scripts": [],
|
||||
"statsJson": true
|
||||
},
|
||||
"configurations": {
|
||||
"production": {
|
||||
|
|
33
angular-client/package-lock.json
generated
33
angular-client/package-lock.json
generated
|
@ -15,6 +15,7 @@
|
|||
"@angular/core": "^19.0.0",
|
||||
"@angular/forms": "^19.0.0",
|
||||
"@angular/material": "^19.0.0",
|
||||
"@angular/material-luxon-adapter": "^19.0.0",
|
||||
"@angular/platform-browser": "^19.0.0",
|
||||
"@angular/platform-browser-dynamic": "^19.0.0",
|
||||
"@angular/router": "^19.0.0",
|
||||
|
@ -27,6 +28,7 @@
|
|||
"@angular/cli": "^19.0.1",
|
||||
"@angular/compiler-cli": "^19.0.0",
|
||||
"@types/jasmine": "~5.1.0",
|
||||
"@types/luxon": "^3.4.2",
|
||||
"jasmine-core": "~5.4.0",
|
||||
"karma": "~6.4.0",
|
||||
"karma-chrome-launcher": "~3.2.0",
|
||||
|
@ -534,6 +536,20 @@
|
|||
"rxjs": "^6.5.3 || ^7.4.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@angular/material-luxon-adapter": {
|
||||
"version": "19.0.0",
|
||||
"resolved": "https://registry.npmjs.org/@angular/material-luxon-adapter/-/material-luxon-adapter-19.0.0.tgz",
|
||||
"integrity": "sha512-sEdw+rfid7XXhVk+4FHMONobvr4OlzGof6PohzGVIp34Nh295pwK7HcgSafq1w8jeJwNPW2obaGa6Zz+uLbW3g==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"tslib": "^2.3.0"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@angular/core": "^19.0.0 || ^20.0.0",
|
||||
"@angular/material": "19.0.0",
|
||||
"luxon": "^3.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@angular/platform-browser": {
|
||||
"version": "19.0.0",
|
||||
"resolved": "https://registry.npmjs.org/@angular/platform-browser/-/platform-browser-19.0.0.tgz",
|
||||
|
@ -5069,6 +5085,13 @@
|
|||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@types/luxon": {
|
||||
"version": "3.4.2",
|
||||
"resolved": "https://registry.npmjs.org/@types/luxon/-/luxon-3.4.2.tgz",
|
||||
"integrity": "sha512-TifLZlFudklWlMBfhubvgqTXRzLDI5pCbGa4P8a3wPyUQSW+1xQ5eDsreP9DWHX3tjq1ke96uYG/nwundroWcA==",
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@types/mime": {
|
||||
"version": "1.3.5",
|
||||
"resolved": "https://registry.npmjs.org/@types/mime/-/mime-1.3.5.tgz",
|
||||
|
@ -9917,6 +9940,16 @@
|
|||
"yallist": "^3.0.2"
|
||||
}
|
||||
},
|
||||
"node_modules/luxon": {
|
||||
"version": "3.5.0",
|
||||
"resolved": "https://registry.npmjs.org/luxon/-/luxon-3.5.0.tgz",
|
||||
"integrity": "sha512-rh+Zjr6DNfUYR3bPwJEnuwDdqMbxZW7LOQfUN4B54+Cl+0o5zaU9RJ6bcidfDtC1cWCZXQ+nvX8bf6bAji37QQ==",
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
}
|
||||
},
|
||||
"node_modules/magic-string": {
|
||||
"version": "0.30.12",
|
||||
"resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.12.tgz",
|
||||
|
|
|
@ -17,6 +17,7 @@
|
|||
"@angular/core": "^19.0.0",
|
||||
"@angular/forms": "^19.0.0",
|
||||
"@angular/material": "^19.0.0",
|
||||
"@angular/material-luxon-adapter": "^19.0.0",
|
||||
"@angular/platform-browser": "^19.0.0",
|
||||
"@angular/platform-browser-dynamic": "^19.0.0",
|
||||
"@angular/router": "^19.0.0",
|
||||
|
@ -29,6 +30,7 @@
|
|||
"@angular/cli": "^19.0.1",
|
||||
"@angular/compiler-cli": "^19.0.0",
|
||||
"@types/jasmine": "~5.1.0",
|
||||
"@types/luxon": "^3.4.2",
|
||||
"jasmine-core": "~5.4.0",
|
||||
"karma": "~6.4.0",
|
||||
"karma-chrome-launcher": "~3.2.0",
|
||||
|
|
|
@ -1,12 +1,13 @@
|
|||
import { ApplicationConfig, provideZoneChangeDetection } from '@angular/core';
|
||||
import { provideRouter } from '@angular/router';
|
||||
|
||||
import { routes } from './app.routes';
|
||||
import { routes } from './routes/app.routes';
|
||||
import { provideHttpClient } from '@angular/common/http';
|
||||
import { Configuration } from '../../build/client';
|
||||
import { provideAnimationsAsync } from '@angular/platform-browser/animations/async';
|
||||
import { provideLuxonDateAdapter } from '@angular/material-luxon-adapter';
|
||||
|
||||
export const appConfig: ApplicationConfig = {
|
||||
providers: [provideZoneChangeDetection({ eventCoalescing: true }), provideRouter(routes), provideHttpClient(), {provide: Configuration, useValue: new Configuration({
|
||||
})}, provideAnimationsAsync()]
|
||||
})}, provideAnimationsAsync(), provideLuxonDateAdapter()]
|
||||
};
|
||||
|
|
|
@ -1,13 +0,0 @@
|
|||
import { Routes } from '@angular/router';
|
||||
import { PageNotFoundComponent } from './page-not-found/page-not-found.component';
|
||||
import { WelcomeComponent } from './welcome/welcome.component';
|
||||
import { OwnerListComponent } from './owner-list/owner-list.component';
|
||||
import { OwnerAddComponent } from './owner-add/owner-add.component';
|
||||
|
||||
export const routes: Routes = [
|
||||
{ path: 'welcome', component: WelcomeComponent },
|
||||
{ path: 'owners', component: OwnerListComponent },
|
||||
{ path: 'owners/add', component: OwnerAddComponent },
|
||||
{ path: '', component: WelcomeComponent },
|
||||
{ path: '**', component: PageNotFoundComponent }
|
||||
];
|
|
@ -0,0 +1,32 @@
|
|||
<h2>Owner Information</h2>
|
||||
<table>
|
||||
<tr>
|
||||
<th>Name</th>
|
||||
<td>{{owner.firstName}} {{owner.lastName}}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th>Address</th>
|
||||
<td>{{owner.address}}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th>City</th>
|
||||
<td>{{owner.city}}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th>Telephone</th>
|
||||
<td>{{owner.telephone}}</td>
|
||||
</tr>
|
||||
</table>
|
||||
|
||||
<a mat-button routerLink="/owners">Back</a>
|
||||
<a mat-button routerLink="/owners/{{owner.id}}/edit">Edit Owner</a>
|
||||
<a mat-button routerLink="/owners/{{owner.id}}/pets/add">Add Pet</a>
|
||||
<mat-divider></mat-divider>
|
||||
<h2>Pets and Visits</h2>
|
||||
<table>
|
||||
@for(pet of owner.pets; track pet.id) {
|
||||
<tr>
|
||||
<td><app-pet-list [pet]="pet"></app-pet-list></td>
|
||||
</tr>
|
||||
}
|
||||
</table>
|
|
@ -0,0 +1,23 @@
|
|||
import { ComponentFixture, TestBed } from '@angular/core/testing';
|
||||
|
||||
import { OwnerDetailComponent } from './owner-detail.component';
|
||||
|
||||
describe('OwnerDetailComponent', () => {
|
||||
let component: OwnerDetailComponent;
|
||||
let fixture: ComponentFixture<OwnerDetailComponent>;
|
||||
|
||||
beforeEach(async () => {
|
||||
await TestBed.configureTestingModule({
|
||||
imports: [OwnerDetailComponent]
|
||||
})
|
||||
.compileComponents();
|
||||
|
||||
fixture = TestBed.createComponent(OwnerDetailComponent);
|
||||
component = fixture.componentInstance;
|
||||
fixture.detectChanges();
|
||||
});
|
||||
|
||||
it('should create', () => {
|
||||
expect(component).toBeTruthy();
|
||||
});
|
||||
});
|
|
@ -0,0 +1,35 @@
|
|||
import { Component, DestroyRef, inject, OnInit } from '@angular/core';
|
||||
import { MatButtonModule } from '@angular/material/button';
|
||||
import { MatDividerModule } from '@angular/material/divider';
|
||||
import { ActivatedRoute, RouterLink } from '@angular/router';
|
||||
import { Owner, OwnerService } from '../../../build/client';
|
||||
import { map, switchMap } from 'rxjs';
|
||||
import { MatSnackBar } from '@angular/material/snack-bar';
|
||||
import { PetListComponent } from '../pet-list/pet-list.component';
|
||||
import { takeUntilDestroyed } from '@angular/core/rxjs-interop';
|
||||
|
||||
@Component({
|
||||
selector: 'app-owner-detail',
|
||||
imports: [RouterLink, MatButtonModule, MatDividerModule, PetListComponent],
|
||||
templateUrl: './owner-detail.component.html',
|
||||
styleUrl: './owner-detail.component.scss'
|
||||
})
|
||||
export class OwnerDetailComponent implements OnInit {
|
||||
owner = {} as Owner;
|
||||
private readonly route = inject(ActivatedRoute);
|
||||
private readonly ownerService = inject(OwnerService);
|
||||
private readonly snackBar = inject(MatSnackBar);
|
||||
private readonly destroyRef = inject(DestroyRef);
|
||||
|
||||
ngOnInit(): void {
|
||||
this.route.params.pipe(
|
||||
map(params => params["id"]),
|
||||
switchMap(id => this.ownerService.getOwner(id)),
|
||||
takeUntilDestroyed(this.destroyRef)
|
||||
).subscribe({
|
||||
next: owner => this.owner = owner,
|
||||
error: error => this.snackBar.open(error)
|
||||
});
|
||||
}
|
||||
|
||||
}
|
55
angular-client/src/app/owner-edit/owner-edit.component.html
Normal file
55
angular-client/src/app/owner-edit/owner-edit.component.html
Normal file
|
@ -0,0 +1,55 @@
|
|||
<h2>Edit Owner</h2>
|
||||
<form [formGroup]="form" (submit)="form.valid && onSubmit();$event.preventDefault()">
|
||||
<mat-form-field>
|
||||
<mat-label>First Name</mat-label>
|
||||
<input matInput formControlName="firstName" />
|
||||
@if (form.controls["firstName"].hasError('required')) {<mat-error>First name is required</mat-error>}
|
||||
@if (form.controls["firstName"].hasError('minlength')) {<mat-error>First name must be at least 1 character
|
||||
long</mat-error>}
|
||||
@if (form.controls["firstName"].hasError('maxlength')) {<mat-error>First name may be at most 30 characters
|
||||
long</mat-error>}
|
||||
@if (form.controls["firstName"].hasError('pattern')) {<mat-error>First name must consist of letters
|
||||
only</mat-error>}
|
||||
</mat-form-field>
|
||||
<br />
|
||||
<mat-form-field>
|
||||
<mat-label>Last Name</mat-label>
|
||||
<input matInput formControlName="lastName" />
|
||||
@if (form.controls["lastName"].hasError('required')) {<mat-error>Last name is required</mat-error>}
|
||||
@if (form.controls["lastName"].hasError('minlength')) {<mat-error>Last name must be at least 1 character
|
||||
long</mat-error>}
|
||||
@if (form.controls["lastName"].hasError('maxlength')) {<mat-error>Last name may be at most 30 characters
|
||||
long</mat-error>}
|
||||
@if (form.controls["lastName"].hasError('pattern')) {<mat-error>Last name must consist of letters
|
||||
only</mat-error>}
|
||||
</mat-form-field>
|
||||
<br />
|
||||
<mat-form-field>
|
||||
<mat-label>Address</mat-label>
|
||||
<input matInput formControlName="address" />
|
||||
@if (form.controls["address"].hasError('required')) {<mat-error>Address is required</mat-error>}
|
||||
@if (form.controls["address"].hasError('maxlength')) {<mat-error>Address may be at most 255 characters
|
||||
long</mat-error>}
|
||||
</mat-form-field>
|
||||
<br />
|
||||
<mat-form-field>
|
||||
<mat-label>City</mat-label>
|
||||
<input matInput formControlName="city" />
|
||||
@if (form.controls["city"].hasError('required')) {<mat-error>City is required</mat-error>}
|
||||
@if (form.controls["city"].hasError('maxlength')) {<mat-error>City may be at most 80 characters
|
||||
long</mat-error>}
|
||||
</mat-form-field>
|
||||
<br />
|
||||
<mat-form-field>
|
||||
<mat-label>Telephone</mat-label>
|
||||
<input matInput formControlName="telephone" type="tel" />
|
||||
@if (form.controls["telephone"].hasError('required')) {<mat-error>Telephone is required</mat-error>}
|
||||
@if (form.controls["telephone"].hasError('maxlength')) {<mat-error>Telephone may be at most 20 numbers
|
||||
long</mat-error>}
|
||||
@if (form.controls["telephone"].hasError('pattern')) {<mat-error>Telephone must consist of numbers
|
||||
only</mat-error>}
|
||||
</mat-form-field>
|
||||
<br />
|
||||
<a mat-button routerLink="/owners/{{owner.id}}">Back</a>
|
||||
<button mat-button type="submit" [disabled]="!form.valid">Update Owner</button>
|
||||
</form>
|
|
@ -0,0 +1,23 @@
|
|||
import { ComponentFixture, TestBed } from '@angular/core/testing';
|
||||
|
||||
import { OwnerEditComponent } from './owner-edit.component';
|
||||
|
||||
describe('OwnerEditComponent', () => {
|
||||
let component: OwnerEditComponent;
|
||||
let fixture: ComponentFixture<OwnerEditComponent>;
|
||||
|
||||
beforeEach(async () => {
|
||||
await TestBed.configureTestingModule({
|
||||
imports: [OwnerEditComponent]
|
||||
})
|
||||
.compileComponents();
|
||||
|
||||
fixture = TestBed.createComponent(OwnerEditComponent);
|
||||
component = fixture.componentInstance;
|
||||
fixture.detectChanges();
|
||||
});
|
||||
|
||||
it('should create', () => {
|
||||
expect(component).toBeTruthy();
|
||||
});
|
||||
});
|
54
angular-client/src/app/owner-edit/owner-edit.component.ts
Normal file
54
angular-client/src/app/owner-edit/owner-edit.component.ts
Normal file
|
@ -0,0 +1,54 @@
|
|||
import { Component, DestroyRef, inject, OnInit } from '@angular/core';
|
||||
import { FormBuilder, FormGroup, ReactiveFormsModule, Validators } from '@angular/forms';
|
||||
import { MatButtonModule } from '@angular/material/button';
|
||||
import { MatFormFieldModule } from '@angular/material/form-field';
|
||||
import { MatInputModule } from '@angular/material/input';
|
||||
import { ActivatedRoute, Router, RouterLink } from '@angular/router';
|
||||
import { Owner, OwnerService } from '../../../build/client';
|
||||
import { MatSnackBar } from '@angular/material/snack-bar';
|
||||
import { map, switchMap } from 'rxjs';
|
||||
import { takeUntilDestroyed } from '@angular/core/rxjs-interop';
|
||||
|
||||
@Component({
|
||||
selector: 'app-owner-edit',
|
||||
imports: [ReactiveFormsModule, MatFormFieldModule, MatInputModule, MatButtonModule, RouterLink],
|
||||
templateUrl: './owner-edit.component.html',
|
||||
styleUrl: './owner-edit.component.scss'
|
||||
})
|
||||
export class OwnerEditComponent implements OnInit {
|
||||
form: FormGroup;
|
||||
owner = {} as Owner;
|
||||
private readonly destroyRef = inject(DestroyRef);
|
||||
private readonly ownerService = inject(OwnerService);
|
||||
private readonly snackBar = inject(MatSnackBar);
|
||||
private readonly route = inject(ActivatedRoute);
|
||||
private readonly router = inject(Router);
|
||||
|
||||
constructor(builder: FormBuilder) {
|
||||
this.form = builder.group({
|
||||
firstName: [null, [Validators.minLength(1), Validators.maxLength(30), Validators.pattern(/^[a-zA-Z]*$/), Validators.required]],
|
||||
lastName: [null, [Validators.minLength(1), Validators.maxLength(30), Validators.pattern(/^[a-zA-Z]*$/), Validators.required]],
|
||||
address: [null, [Validators.required, Validators.maxLength(255)]],
|
||||
city: [null, [Validators.required, Validators.maxLength(80)]],
|
||||
telephone: [null, [Validators.minLength(1), Validators.maxLength(20), Validators.pattern(/^[0-9]*$/), Validators.required]],
|
||||
});
|
||||
}
|
||||
|
||||
ngOnInit(): void {
|
||||
this.route.params.pipe(
|
||||
map(params => params["id"]),
|
||||
switchMap(id => this.ownerService.getOwner(id)),
|
||||
takeUntilDestroyed(this.destroyRef)
|
||||
).subscribe({
|
||||
next: owner => { this.owner = owner; this.form.patchValue(owner) },
|
||||
error: error => this.snackBar.open(error)
|
||||
});
|
||||
}
|
||||
|
||||
onSubmit() {
|
||||
this.ownerService.updateOwner(this.owner.id!, this.form.value).subscribe({
|
||||
next: _ => this.router.navigate(['/owners', this.owner.id!]),
|
||||
error: error => this.snackBar.open(error)
|
||||
});
|
||||
}
|
||||
}
|
|
@ -1,5 +1,3 @@
|
|||
<mat-card>
|
||||
<mat-card-header><mat-card-title>Oops! Page not found !</mat-card-title></mat-card-header>
|
||||
<mat-card-content>Not Found - 404 error</mat-card-content>
|
||||
<img style="margin: 0 auto" src="./assets/images/pets.png" alt="pets logo" />
|
||||
</mat-card>
|
||||
<h1>Oops! Page not found !</h1>
|
||||
<h2>Not Found - 404 error</h2>
|
||||
<img style="margin: 0 auto" src="./assets/images/pets.png" alt="pets logo" />
|
|
@ -3,7 +3,7 @@ import { MatCardModule } from '@angular/material/card';
|
|||
|
||||
@Component({
|
||||
selector: 'app-page-not-found',
|
||||
imports: [MatCardModule],
|
||||
imports: [],
|
||||
templateUrl: './page-not-found.component.html',
|
||||
styleUrl: './page-not-found.component.scss'
|
||||
})
|
||||
|
|
35
angular-client/src/app/pet-add/pet-add.component.html
Normal file
35
angular-client/src/app/pet-add/pet-add.component.html
Normal file
|
@ -0,0 +1,35 @@
|
|||
<h2>Add Pet</h2>
|
||||
<form [formGroup]="form" (submit)="form.valid && onSubmit(); $event.preventDefault()">
|
||||
<mat-form-field>
|
||||
<mat-label>Name</mat-label>
|
||||
<input matInput formControlName="name" />
|
||||
@if (form.controls["name"].hasError('required')) {<mat-error>Name is required</mat-error>}
|
||||
@if (form.controls["name"].hasError('minlength')) {<mat-error>Name must be at least 1 character
|
||||
long</mat-error>}
|
||||
@if (form.controls["name"].hasError('maxlength')) {<mat-error>Name may be at most 30 characters
|
||||
long</mat-error>}
|
||||
@if (form.controls["name"].hasError('pattern')) {<mat-error>Name must begin with a letter</mat-error>}
|
||||
</mat-form-field>
|
||||
<br />
|
||||
<mat-form-field>
|
||||
<mat-label>Birth date</mat-label>
|
||||
<input matInput formControlName="birthDate" [matDatepicker]="picker" />
|
||||
<mat-datepicker-toggle matIconSuffix [for]="picker"></mat-datepicker-toggle>
|
||||
<mat-datepicker #picker></mat-datepicker>
|
||||
@if (form.controls["birthDate"].hasError('required')) {<mat-error>Birth date is required</mat-error>}
|
||||
@if (form.controls["birthDate"].hasError('matDatepickerParse')) {<mat-error>Invalid date format</mat-error>}
|
||||
</mat-form-field>
|
||||
<br />
|
||||
<mat-form-field>
|
||||
<mat-label>Type</mat-label>
|
||||
<select matNativeControl formControlName="type">
|
||||
@for(type of petTypes; track type.id) {
|
||||
<option [ngValue]="type">{{type.name}}</option>
|
||||
}
|
||||
</select>
|
||||
@if (form.controls["type"].hasError('required')) {<mat-error>Type is required</mat-error>}
|
||||
</mat-form-field>
|
||||
<br/>
|
||||
<a mat-button routerLink="/owners/{{owner.id}}">Back</a>
|
||||
<button mat-button type="submit" [disabled]="!form.valid">Save Pet</button>
|
||||
</form>
|
0
angular-client/src/app/pet-add/pet-add.component.scss
Normal file
0
angular-client/src/app/pet-add/pet-add.component.scss
Normal file
23
angular-client/src/app/pet-add/pet-add.component.spec.ts
Normal file
23
angular-client/src/app/pet-add/pet-add.component.spec.ts
Normal file
|
@ -0,0 +1,23 @@
|
|||
import { ComponentFixture, TestBed } from '@angular/core/testing';
|
||||
|
||||
import { PetAddComponent } from './pet-add.component';
|
||||
|
||||
describe('PetAddComponent', () => {
|
||||
let component: PetAddComponent;
|
||||
let fixture: ComponentFixture<PetAddComponent>;
|
||||
|
||||
beforeEach(async () => {
|
||||
await TestBed.configureTestingModule({
|
||||
imports: [PetAddComponent]
|
||||
})
|
||||
.compileComponents();
|
||||
|
||||
fixture = TestBed.createComponent(PetAddComponent);
|
||||
component = fixture.componentInstance;
|
||||
fixture.detectChanges();
|
||||
});
|
||||
|
||||
it('should create', () => {
|
||||
expect(component).toBeTruthy();
|
||||
});
|
||||
});
|
66
angular-client/src/app/pet-add/pet-add.component.ts
Normal file
66
angular-client/src/app/pet-add/pet-add.component.ts
Normal file
|
@ -0,0 +1,66 @@
|
|||
import { Component, DestroyRef, inject, OnInit } from '@angular/core';
|
||||
import { Owner, OwnerService, PetFields, PetService, PetType, PettypesService } from '../../../build/client';
|
||||
import { ActivatedRoute, Router, RouterLink } from '@angular/router';
|
||||
import { takeUntilDestroyed } from '@angular/core/rxjs-interop';
|
||||
import { MatSnackBar } from '@angular/material/snack-bar';
|
||||
import { map, switchMap } from 'rxjs';
|
||||
import { FormBuilder, FormGroup, ReactiveFormsModule, Validators } from '@angular/forms';
|
||||
import { MatFormFieldModule } from '@angular/material/form-field';
|
||||
import { MatInputModule } from '@angular/material/input';
|
||||
import { MatButtonModule } from '@angular/material/button';
|
||||
import { MatDatepickerModule } from '@angular/material/datepicker';
|
||||
import { MatSelectModule } from '@angular/material/select';
|
||||
import { DateTime } from 'luxon';
|
||||
|
||||
|
||||
@Component({
|
||||
selector: 'app-pet-add',
|
||||
imports: [MatSelectModule, ReactiveFormsModule, MatFormFieldModule, MatInputModule, MatButtonModule, RouterLink, MatDatepickerModule],
|
||||
templateUrl: './pet-add.component.html',
|
||||
styleUrl: './pet-add.component.scss'
|
||||
})
|
||||
export class PetAddComponent implements OnInit {
|
||||
form: FormGroup;
|
||||
petTypes = [] as PetType[];
|
||||
owner = {} as Owner;
|
||||
private readonly route = inject(ActivatedRoute);
|
||||
private readonly ownerService = inject(OwnerService);
|
||||
private readonly petService = inject(PetService);
|
||||
private readonly petTypesService = inject(PettypesService);
|
||||
private readonly router = inject(Router);
|
||||
private readonly destroyRef = inject(DestroyRef);
|
||||
private readonly snackBar = inject(MatSnackBar);
|
||||
|
||||
constructor(builder: FormBuilder) {
|
||||
this.form = builder.group({
|
||||
name: [null, [Validators.required, Validators.minLength(1), Validators.maxLength(30), Validators.pattern(/^[A-Za-z].*$/)]],
|
||||
birthDate: [null, Validators.required],
|
||||
type: [null, Validators.required]
|
||||
});
|
||||
}
|
||||
|
||||
ngOnInit(): void {
|
||||
this.petTypesService.listPetTypes().pipe(takeUntilDestroyed(this.destroyRef)).subscribe({
|
||||
next: types => this.petTypes = types,
|
||||
error: error => this.snackBar.open(error)
|
||||
});
|
||||
this.route.params.pipe(
|
||||
map(params => params['id']),
|
||||
switchMap(id => this.ownerService.getOwner(id)),
|
||||
takeUntilDestroyed(this.destroyRef)
|
||||
).subscribe({
|
||||
next: owner => this.owner = owner,
|
||||
error: error => this.snackBar.open(error)
|
||||
});
|
||||
}
|
||||
|
||||
onSubmit() {
|
||||
const pet = this.form.value as PetFields;
|
||||
const formDate = this.form.controls['birthDate'].value as DateTime|string;
|
||||
pet.birthDate = typeof formDate === 'string' ? formDate : formDate.toISODate()!;
|
||||
this.petService.addPetToOwner(this.owner.id!, pet).subscribe({
|
||||
next: _ => this.router.navigate(['/owners', this.owner.id!]),
|
||||
error: error => this.snackBar.open(error)
|
||||
});
|
||||
}
|
||||
}
|
40
angular-client/src/app/pet-edit/pet-edit.component.html
Normal file
40
angular-client/src/app/pet-edit/pet-edit.component.html
Normal file
|
@ -0,0 +1,40 @@
|
|||
<h2>Edit Pet</h2>
|
||||
<form [formGroup]="form" (submit)="form.valid && onSubmit(); $event.preventDefault()">
|
||||
<mat-form-field>
|
||||
<mat-label>Owner</mat-label>
|
||||
<input matInput readonly="readonly" value="{{currentOwner?.firstName}} {{currentOwner?.lastName}}" />
|
||||
</mat-form-field>
|
||||
<br />
|
||||
<mat-form-field>
|
||||
<mat-label>Name</mat-label>
|
||||
<input matInput formControlName="name" />
|
||||
@if (form.controls["name"].hasError('required')) {<mat-error>Name is required</mat-error>}
|
||||
@if (form.controls["name"].hasError('minlength')) {<mat-error>Name must be at least 1 character
|
||||
long</mat-error>}
|
||||
@if (form.controls["name"].hasError('maxlength')) {<mat-error>Name may be at most 30 characters
|
||||
long</mat-error>}
|
||||
@if (form.controls["name"].hasError('pattern')) {<mat-error>Name must begin with a letter</mat-error>}
|
||||
</mat-form-field>
|
||||
<br />
|
||||
<mat-form-field>
|
||||
<mat-label>Birth date</mat-label>
|
||||
<input matInput formControlName="birthDate" [matDatepicker]="picker" />
|
||||
<mat-datepicker-toggle matIconSuffix [for]="picker"></mat-datepicker-toggle>
|
||||
<mat-datepicker #picker></mat-datepicker>
|
||||
@if (form.controls["birthDate"].hasError('required')) {<mat-error>Birth date is required</mat-error>}
|
||||
@if (form.controls["birthDate"].hasError('matDatepickerParse')) {<mat-error>Invalid date format</mat-error>}
|
||||
</mat-form-field>
|
||||
<br />
|
||||
<mat-form-field>
|
||||
<mat-label>Type</mat-label>
|
||||
<select matNativeControl formControlName="type">
|
||||
@for(type of petTypes; track type.id) {
|
||||
<option [ngValue]="type" [selected]="type.id === pet?.type?.id">{{type.name}}</option>
|
||||
}
|
||||
</select>
|
||||
@if (form.controls["type"].hasError('required')) {<mat-error>Type is required</mat-error>}
|
||||
</mat-form-field>
|
||||
<br />
|
||||
<a mat-button routerLink="/owners/{{currentOwner?.id}}">Back</a>
|
||||
<button mat-button type="submit" [disabled]="!form.valid">Update Pet</button>
|
||||
</form>
|
0
angular-client/src/app/pet-edit/pet-edit.component.scss
Normal file
0
angular-client/src/app/pet-edit/pet-edit.component.scss
Normal file
23
angular-client/src/app/pet-edit/pet-edit.component.spec.ts
Normal file
23
angular-client/src/app/pet-edit/pet-edit.component.spec.ts
Normal file
|
@ -0,0 +1,23 @@
|
|||
import { ComponentFixture, TestBed } from '@angular/core/testing';
|
||||
|
||||
import { PetEditComponent } from './pet-edit.component';
|
||||
|
||||
describe('PetEditComponent', () => {
|
||||
let component: PetEditComponent;
|
||||
let fixture: ComponentFixture<PetEditComponent>;
|
||||
|
||||
beforeEach(async () => {
|
||||
await TestBed.configureTestingModule({
|
||||
imports: [PetEditComponent]
|
||||
})
|
||||
.compileComponents();
|
||||
|
||||
fixture = TestBed.createComponent(PetEditComponent);
|
||||
component = fixture.componentInstance;
|
||||
fixture.detectChanges();
|
||||
});
|
||||
|
||||
it('should create', () => {
|
||||
expect(component).toBeTruthy();
|
||||
});
|
||||
});
|
69
angular-client/src/app/pet-edit/pet-edit.component.ts
Normal file
69
angular-client/src/app/pet-edit/pet-edit.component.ts
Normal file
|
@ -0,0 +1,69 @@
|
|||
import { Component, DestroyRef, inject } from '@angular/core';
|
||||
import { FormBuilder, FormGroup, ReactiveFormsModule, Validators } from '@angular/forms';
|
||||
import { MatButtonModule } from '@angular/material/button';
|
||||
import { MatDatepickerModule } from '@angular/material/datepicker';
|
||||
import { MatFormFieldModule } from '@angular/material/form-field';
|
||||
import { MatInputModule } from '@angular/material/input';
|
||||
import { MatSelectModule } from '@angular/material/select';
|
||||
import { ActivatedRoute, Router, RouterLink } from '@angular/router';
|
||||
import { Owner, OwnerService, Pet, PetService, PetType, PettypesService } from '../../../build/client';
|
||||
import { takeUntilDestroyed } from '@angular/core/rxjs-interop';
|
||||
import { MatSnackBar } from '@angular/material/snack-bar';
|
||||
import { forkJoin, map, of, switchMap } from 'rxjs';
|
||||
import { DateTime } from 'luxon';
|
||||
|
||||
@Component({
|
||||
selector: 'app-pet-edit',
|
||||
imports: [MatSelectModule, ReactiveFormsModule, MatFormFieldModule, MatInputModule, MatButtonModule, RouterLink, MatDatepickerModule],
|
||||
templateUrl: './pet-edit.component.html',
|
||||
styleUrl: './pet-edit.component.scss'
|
||||
})
|
||||
export class PetEditComponent {
|
||||
form: FormGroup;
|
||||
pet?: Pet;
|
||||
currentOwner?: Owner;
|
||||
petTypes = [] as PetType[];
|
||||
private readonly route = inject(ActivatedRoute);
|
||||
private readonly petService = inject(PetService);
|
||||
private readonly petTypesService = inject(PettypesService);
|
||||
private readonly router = inject(Router);
|
||||
private readonly destroyRef = inject(DestroyRef);
|
||||
private readonly snackBar = inject(MatSnackBar);
|
||||
private readonly ownerService = inject(OwnerService);
|
||||
|
||||
constructor(builder: FormBuilder) {
|
||||
this.form = builder.group({
|
||||
name: [null, [Validators.required, Validators.minLength(1), Validators.maxLength(30), Validators.pattern(/^[A-Za-z].*$/)]],
|
||||
birthDate: [null, Validators.required],
|
||||
type: [null, Validators.required],
|
||||
ownerId: [null, Validators.required]
|
||||
});
|
||||
}
|
||||
|
||||
ngOnInit(): void {
|
||||
this.petTypesService.listPetTypes().pipe(takeUntilDestroyed(this.destroyRef)).subscribe({
|
||||
next: types => this.petTypes = types,
|
||||
error: error => this.snackBar.open(error)
|
||||
});
|
||||
this.route.params.pipe(
|
||||
map(params => params['id']),
|
||||
switchMap(id => this.petService.getPet(id)),
|
||||
switchMap(pet => forkJoin([of(pet), this.ownerService.getOwner(pet.ownerId!)])),
|
||||
takeUntilDestroyed(this.destroyRef)
|
||||
).subscribe({
|
||||
next: ([pet, owner]) => { this.pet = pet; this.form.patchValue(pet); this.currentOwner = owner; },
|
||||
error: error => this.snackBar.open(error)
|
||||
});
|
||||
}
|
||||
|
||||
onSubmit() {
|
||||
const pet = this.form.value as Pet;
|
||||
const formDate = this.form.controls['birthDate'].value as DateTime|string;
|
||||
pet.birthDate = typeof formDate === 'string' ? formDate : formDate.toISODate()!;
|
||||
this.petService.updatePet(this.pet!.id, pet).subscribe({
|
||||
next: _ => this.router.navigate(['/owners', this.pet?.ownerId]),
|
||||
error: error => this.snackBar.open(error)
|
||||
});
|
||||
}
|
||||
|
||||
}
|
22
angular-client/src/app/pet-list/pet-list.component.html
Normal file
22
angular-client/src/app/pet-list/pet-list.component.html
Normal file
|
@ -0,0 +1,22 @@
|
|||
@if(pet != null) {
|
||||
<table>
|
||||
<tr>
|
||||
<td valign="top">
|
||||
<dl>
|
||||
<dt>Name</dt>
|
||||
<dd>{{pet.name}}</dd>
|
||||
<dt>Birth Date</dt>
|
||||
<dd>{{pet.birthDate}}</dd>
|
||||
<dt>Type</dt>
|
||||
<dd>{{pet.type.name}}</dd>
|
||||
<a mat-button routerLink="/pets/{{pet.id}}/edit">Edit Pet</a>
|
||||
<button mat-button (click)="deletePet(pet)">Delete Pet</button>
|
||||
<a mat-button routerLink="/pets/{{pet.id}}/visits/add">Add Visit</a>
|
||||
</dl>
|
||||
</td>
|
||||
<td valign="top">
|
||||
<app-visit-list [visits]="pet.visits"></app-visit-list>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
}
|
0
angular-client/src/app/pet-list/pet-list.component.scss
Normal file
0
angular-client/src/app/pet-list/pet-list.component.scss
Normal file
23
angular-client/src/app/pet-list/pet-list.component.spec.ts
Normal file
23
angular-client/src/app/pet-list/pet-list.component.spec.ts
Normal file
|
@ -0,0 +1,23 @@
|
|||
import { ComponentFixture, TestBed } from '@angular/core/testing';
|
||||
|
||||
import { PetListComponent } from './pet-list.component';
|
||||
|
||||
describe('PetListComponent', () => {
|
||||
let component: PetListComponent;
|
||||
let fixture: ComponentFixture<PetListComponent>;
|
||||
|
||||
beforeEach(async () => {
|
||||
await TestBed.configureTestingModule({
|
||||
imports: [PetListComponent]
|
||||
})
|
||||
.compileComponents();
|
||||
|
||||
fixture = TestBed.createComponent(PetListComponent);
|
||||
component = fixture.componentInstance;
|
||||
fixture.detectChanges();
|
||||
});
|
||||
|
||||
it('should create', () => {
|
||||
expect(component).toBeTruthy();
|
||||
});
|
||||
});
|
27
angular-client/src/app/pet-list/pet-list.component.ts
Normal file
27
angular-client/src/app/pet-list/pet-list.component.ts
Normal file
|
@ -0,0 +1,27 @@
|
|||
import { Component, inject, Input } from '@angular/core';
|
||||
import { RouterLink } from '@angular/router';
|
||||
import { Pet, PetService } from '../../../build/client';
|
||||
import { MatSnackBar } from '@angular/material/snack-bar';
|
||||
import { MatButtonModule } from '@angular/material/button';
|
||||
import { VisitListComponent } from '../visit-list/visit-list.component';
|
||||
|
||||
@Component({
|
||||
selector: 'app-pet-list',
|
||||
imports: [RouterLink, MatButtonModule, VisitListComponent],
|
||||
templateUrl: './pet-list.component.html',
|
||||
styleUrl: './pet-list.component.scss'
|
||||
})
|
||||
export class PetListComponent {
|
||||
@Input() pet ?: Pet;
|
||||
private readonly petService = inject(PetService);
|
||||
private readonly snackBar = inject(MatSnackBar);
|
||||
|
||||
deletePet(pet: Pet) {
|
||||
this.petService.deletePet(pet.id).subscribe({
|
||||
next: _response => {
|
||||
this.pet = undefined;
|
||||
},
|
||||
error: error => this.snackBar.open(error)
|
||||
});
|
||||
}
|
||||
}
|
12
angular-client/src/app/routes/app.routes.ts
Normal file
12
angular-client/src/app/routes/app.routes.ts
Normal file
|
@ -0,0 +1,12 @@
|
|||
import { Routes } from '@angular/router';
|
||||
import { PageNotFoundComponent } from '../page-not-found/page-not-found.component';
|
||||
import { WelcomeComponent } from '../welcome/welcome.component';
|
||||
|
||||
export const routes: Routes = [
|
||||
{ path: 'welcome', component: WelcomeComponent },
|
||||
{ path: 'owners', loadChildren: () => import('./owner.routes').then(x => x.ownerRoutes) },
|
||||
{ path: 'pets', loadChildren: () => import('./pet.routes').then(x => x.petRoutes) },
|
||||
{ path: 'visit', loadChildren: () => import('./visit.routes').then(x => x.visitRoutes) },
|
||||
{ path: '', component: WelcomeComponent },
|
||||
{ path: '**', component: PageNotFoundComponent }
|
||||
];
|
14
angular-client/src/app/routes/owner.routes.ts
Normal file
14
angular-client/src/app/routes/owner.routes.ts
Normal file
|
@ -0,0 +1,14 @@
|
|||
import { Routes } from "@angular/router";
|
||||
import { OwnerAddComponent } from "../owner-add/owner-add.component";
|
||||
import { OwnerDetailComponent } from "../owner-detail/owner-detail.component";
|
||||
import { OwnerEditComponent } from "../owner-edit/owner-edit.component";
|
||||
import { OwnerListComponent } from "../owner-list/owner-list.component";
|
||||
import { PetAddComponent } from "../pet-add/pet-add.component";
|
||||
|
||||
export const ownerRoutes: Routes = [
|
||||
{ path: '', component: OwnerListComponent },
|
||||
{ path: 'add', component: OwnerAddComponent },
|
||||
{ path: ':id', component: OwnerDetailComponent },
|
||||
{ path: ':id/edit', component: OwnerEditComponent },
|
||||
{ path: ':id/pets/add', component: PetAddComponent },
|
||||
];
|
8
angular-client/src/app/routes/pet.routes.ts
Normal file
8
angular-client/src/app/routes/pet.routes.ts
Normal file
|
@ -0,0 +1,8 @@
|
|||
import { Routes } from "@angular/router";
|
||||
import { PetEditComponent } from "../pet-edit/pet-edit.component";
|
||||
import { VisitAddComponent } from "../visit-add/visit-add.component";
|
||||
|
||||
export const petRoutes: Routes = [
|
||||
{ path: ':id/edit', component: PetEditComponent },
|
||||
{ path: ':id/visits/add', component: VisitAddComponent }
|
||||
];
|
6
angular-client/src/app/routes/visit.routes.ts
Normal file
6
angular-client/src/app/routes/visit.routes.ts
Normal file
|
@ -0,0 +1,6 @@
|
|||
import { Routes } from "@angular/router";
|
||||
import { VisitEditComponent } from "../visit-edit/visit-edit.component";
|
||||
|
||||
export const visitRoutes: Routes = [
|
||||
{path: ':id/edit', component: VisitEditComponent}
|
||||
];
|
49
angular-client/src/app/visit-add/visit-add.component.html
Normal file
49
angular-client/src/app/visit-add/visit-add.component.html
Normal file
|
@ -0,0 +1,49 @@
|
|||
<h2>New Visit</h2>
|
||||
@if(currentPet && currentOwner) {
|
||||
<b>Pet</b>
|
||||
<table>
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Name</th>
|
||||
<th>Birth Date</th>
|
||||
<th>Type</th>
|
||||
<th>Owner</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tr>
|
||||
<td>{{currentPet.name}}</td>
|
||||
<td>{{currentPet.birthDate}}</td>
|
||||
<td>{{currentPet.type.name}}</td>
|
||||
<td>{{currentOwner.firstName}} {{currentOwner.lastName}}</td>
|
||||
</tr>
|
||||
</table>
|
||||
|
||||
<form [formGroup]="form" (submit)="form.valid && onSubmit();$event.preventDefault()">
|
||||
<mat-form-field>
|
||||
<mat-label>Date</mat-label>
|
||||
<input matInput formControlName="date" [matDatepicker]="picker" />
|
||||
<mat-datepicker-toggle matIconSuffix [for]="picker"></mat-datepicker-toggle>
|
||||
<mat-datepicker #picker></mat-datepicker>
|
||||
@if (form.controls["date"].hasError('required')) {<mat-error>Date is required</mat-error>}
|
||||
@if (form.controls["date"].hasError('matDatepickerParse')) {<mat-error>Invalid date format</mat-error>}
|
||||
</mat-form-field>
|
||||
<br />
|
||||
<mat-form-field>
|
||||
<mat-label>Description</mat-label>
|
||||
<input matInput formControlName="description" />
|
||||
@if (form.controls["description"].hasError('required')) {<mat-error>Description is required</mat-error>}
|
||||
@if (form.controls["description"].hasError('minlength')) {<mat-error>Description must be at least 1 character
|
||||
long</mat-error>}
|
||||
@if (form.controls["description"].hasError('maxlength')) {<mat-error>Description may be at most 255 characters
|
||||
long</mat-error>}
|
||||
</mat-form-field>
|
||||
<br/>
|
||||
<a mat-button routerLink="/owners/{{currentOwner.id}}">Back</a>
|
||||
<button mat-button type="submit" [disabled]="!form.valid">Add Visit</button>
|
||||
</form>
|
||||
|
||||
<br />
|
||||
<p><b>Previous Visits</b></p>
|
||||
<br />
|
||||
<app-visit-list [visits]="currentPet.visits"></app-visit-list>
|
||||
}
|
23
angular-client/src/app/visit-add/visit-add.component.spec.ts
Normal file
23
angular-client/src/app/visit-add/visit-add.component.spec.ts
Normal file
|
@ -0,0 +1,23 @@
|
|||
import { ComponentFixture, TestBed } from '@angular/core/testing';
|
||||
|
||||
import { VisitAddComponent } from './visit-add.component';
|
||||
|
||||
describe('VisitAddComponent', () => {
|
||||
let component: VisitAddComponent;
|
||||
let fixture: ComponentFixture<VisitAddComponent>;
|
||||
|
||||
beforeEach(async () => {
|
||||
await TestBed.configureTestingModule({
|
||||
imports: [VisitAddComponent]
|
||||
})
|
||||
.compileComponents();
|
||||
|
||||
fixture = TestBed.createComponent(VisitAddComponent);
|
||||
component = fixture.componentInstance;
|
||||
fixture.detectChanges();
|
||||
});
|
||||
|
||||
it('should create', () => {
|
||||
expect(component).toBeTruthy();
|
||||
});
|
||||
});
|
63
angular-client/src/app/visit-add/visit-add.component.ts
Normal file
63
angular-client/src/app/visit-add/visit-add.component.ts
Normal file
|
@ -0,0 +1,63 @@
|
|||
import { Component, DestroyRef, inject, OnInit } from '@angular/core';
|
||||
import { FormBuilder, FormGroup, ReactiveFormsModule, Validators } from '@angular/forms';
|
||||
import { MatButtonModule } from '@angular/material/button';
|
||||
import { MatFormFieldModule } from '@angular/material/form-field';
|
||||
import { ActivatedRoute, Router, RouterLink } from '@angular/router';
|
||||
import { VisitListComponent } from '../visit-list/visit-list.component';
|
||||
import { MatDatepickerModule } from '@angular/material/datepicker';
|
||||
import { Owner, OwnerService, Pet, PetService, VisitFields, VisitService } from '../../../build/client';
|
||||
import { MatSnackBar } from '@angular/material/snack-bar';
|
||||
import { forkJoin, map, of, switchMap } from 'rxjs';
|
||||
import { takeUntilDestroyed } from '@angular/core/rxjs-interop';
|
||||
import { DateTime } from 'luxon';
|
||||
import { MatInputModule } from '@angular/material/input';
|
||||
|
||||
@Component({
|
||||
selector: 'app-visit-add',
|
||||
imports: [ReactiveFormsModule, MatInputModule, MatFormFieldModule, MatButtonModule, RouterLink, VisitListComponent, MatDatepickerModule],
|
||||
templateUrl: './visit-add.component.html',
|
||||
styleUrl: './visit-add.component.scss'
|
||||
})
|
||||
export class VisitAddComponent implements OnInit {
|
||||
currentPet?: Pet;
|
||||
currentOwner?: Owner;
|
||||
form: FormGroup;
|
||||
private readonly visitService = inject(VisitService);
|
||||
private readonly petService = inject(PetService);
|
||||
private readonly ownerService = inject(OwnerService);
|
||||
private readonly route = inject(ActivatedRoute);
|
||||
private readonly router = inject(Router);
|
||||
private readonly snackBar = inject(MatSnackBar);
|
||||
private readonly destroyRef = inject(DestroyRef);
|
||||
|
||||
constructor(builder: FormBuilder) {
|
||||
this.form = builder.group({
|
||||
date: [null, Validators.required],
|
||||
description: [null, [Validators.required, Validators.minLength(1), Validators.maxLength(255)]]
|
||||
});
|
||||
}
|
||||
|
||||
ngOnInit(): void {
|
||||
this.route.params.pipe(
|
||||
map(params => params['id']),
|
||||
switchMap(id => this.petService.getPet(id)),
|
||||
switchMap(pet => forkJoin([of(pet), this.ownerService.getOwner(pet.ownerId!)])),
|
||||
takeUntilDestroyed(this.destroyRef)
|
||||
).subscribe({
|
||||
next: ([pet, owner]) => { this.currentPet = pet; this.currentOwner = owner },
|
||||
error: error => this.snackBar.open(error)
|
||||
});
|
||||
}
|
||||
|
||||
onSubmit() {
|
||||
const formDate = this.form.controls['date'].value as DateTime|string;
|
||||
const visit: VisitFields = {
|
||||
date: typeof formDate === 'string' ? formDate : formDate.toISODate()!,
|
||||
description: this.form.controls['description'].value
|
||||
};
|
||||
this.visitService.addVisitToOwner(this.currentOwner!.id!, this.currentPet!.id, visit).subscribe({
|
||||
next: _ => this.router.navigate(['/owners', this.currentOwner!.id!]),
|
||||
error: error => this.snackBar.open(error)
|
||||
});
|
||||
}
|
||||
}
|
44
angular-client/src/app/visit-edit/visit-edit.component.html
Normal file
44
angular-client/src/app/visit-edit/visit-edit.component.html
Normal file
|
@ -0,0 +1,44 @@
|
|||
<h2>Edit Visit</h2>
|
||||
@if(currentPet && currentOwner) {
|
||||
<b>Pet</b>
|
||||
<table>
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Name</th>
|
||||
<th>Birth Date</th>
|
||||
<th>Type</th>
|
||||
<th>Owner</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tr>
|
||||
<td>{{currentPet.name}}</td>
|
||||
<td>{{currentPet.birthDate}}</td>
|
||||
<td>{{currentPet.type.name}}</td>
|
||||
<td>{{currentOwner.firstName}} {{currentOwner.lastName}}</td>
|
||||
</tr>
|
||||
</table>
|
||||
|
||||
<form [formGroup]="form" (submit)="form.valid && onSubmit();$event.preventDefault()">
|
||||
<mat-form-field>
|
||||
<mat-label>Date</mat-label>
|
||||
<input matInput formControlName="date" [matDatepicker]="picker" />
|
||||
<mat-datepicker-toggle matIconSuffix [for]="picker"></mat-datepicker-toggle>
|
||||
<mat-datepicker #picker></mat-datepicker>
|
||||
@if (form.controls["date"].hasError('required')) {<mat-error>Date is required</mat-error>}
|
||||
@if (form.controls["date"].hasError('matDatepickerParse')) {<mat-error>Invalid date format</mat-error>}
|
||||
</mat-form-field>
|
||||
<br />
|
||||
<mat-form-field>
|
||||
<mat-label>Description</mat-label>
|
||||
<input matInput formControlName="description" />
|
||||
@if (form.controls["description"].hasError('required')) {<mat-error>Description is required</mat-error>}
|
||||
@if (form.controls["description"].hasError('minlength')) {<mat-error>Description must be at least 1 character
|
||||
long</mat-error>}
|
||||
@if (form.controls["description"].hasError('maxlength')) {<mat-error>Description may be at most 255 characters
|
||||
long</mat-error>}
|
||||
</mat-form-field>
|
||||
<br />
|
||||
<a mat-button routerLink="/owners/{{currentOwner.id}}">Back</a>
|
||||
<button mat-button type="submit" [disabled]="!form.valid">Update Visit</button>
|
||||
</form>
|
||||
}
|
|
@ -0,0 +1,23 @@
|
|||
import { ComponentFixture, TestBed } from '@angular/core/testing';
|
||||
|
||||
import { VisitEditComponent } from './visit-edit.component';
|
||||
|
||||
describe('VisitEditComponent', () => {
|
||||
let component: VisitEditComponent;
|
||||
let fixture: ComponentFixture<VisitEditComponent>;
|
||||
|
||||
beforeEach(async () => {
|
||||
await TestBed.configureTestingModule({
|
||||
imports: [VisitEditComponent]
|
||||
})
|
||||
.compileComponents();
|
||||
|
||||
fixture = TestBed.createComponent(VisitEditComponent);
|
||||
component = fixture.componentInstance;
|
||||
fixture.detectChanges();
|
||||
});
|
||||
|
||||
it('should create', () => {
|
||||
expect(component).toBeTruthy();
|
||||
});
|
||||
});
|
66
angular-client/src/app/visit-edit/visit-edit.component.ts
Normal file
66
angular-client/src/app/visit-edit/visit-edit.component.ts
Normal file
|
@ -0,0 +1,66 @@
|
|||
import { Component, DestroyRef, inject, OnInit } from '@angular/core';
|
||||
import { FormBuilder, FormGroup, ReactiveFormsModule, Validators } from '@angular/forms';
|
||||
import { MatButtonModule } from '@angular/material/button';
|
||||
import { MatDatepickerModule } from '@angular/material/datepicker';
|
||||
import { MatFormFieldModule } from '@angular/material/form-field';
|
||||
import { MatInputModule } from '@angular/material/input';
|
||||
import { ActivatedRoute, Router, RouterLink } from '@angular/router';
|
||||
import { VisitListComponent } from '../visit-list/visit-list.component';
|
||||
import { takeUntilDestroyed } from '@angular/core/rxjs-interop';
|
||||
import { MatSnackBar } from '@angular/material/snack-bar';
|
||||
import { DateTime } from 'luxon';
|
||||
import { map, switchMap, forkJoin, of } from 'rxjs';
|
||||
import { Pet, Owner, VisitService, PetService, OwnerService, VisitFields, Visit } from '../../../build/client';
|
||||
|
||||
@Component({
|
||||
selector: 'app-visit-edit',
|
||||
imports: [ReactiveFormsModule, MatInputModule, MatFormFieldModule, MatButtonModule, RouterLink, MatDatepickerModule],
|
||||
templateUrl: './visit-edit.component.html',
|
||||
styleUrl: './visit-edit.component.scss'
|
||||
})
|
||||
export class VisitEditComponent implements OnInit {
|
||||
|
||||
currentPet?: Pet;
|
||||
currentOwner?: Owner;
|
||||
visit?: Visit;
|
||||
form: FormGroup;
|
||||
private readonly visitService = inject(VisitService);
|
||||
private readonly petService = inject(PetService);
|
||||
private readonly ownerService = inject(OwnerService);
|
||||
private readonly route = inject(ActivatedRoute);
|
||||
private readonly router = inject(Router);
|
||||
private readonly snackBar = inject(MatSnackBar);
|
||||
private readonly destroyRef = inject(DestroyRef);
|
||||
|
||||
constructor(builder: FormBuilder) {
|
||||
this.form = builder.group({
|
||||
date: [null, Validators.required],
|
||||
description: [null, [Validators.required, Validators.minLength(1), Validators.maxLength(255)]]
|
||||
});
|
||||
}
|
||||
|
||||
ngOnInit(): void {
|
||||
this.route.params.pipe(
|
||||
map(params => params['id']),
|
||||
switchMap(id => this.visitService.getVisit(id)),
|
||||
switchMap(visit => forkJoin([of(visit), this.petService.getPet(visit.petId!)])),
|
||||
switchMap(([visit, pet]) => forkJoin([of(visit), of(pet), this.ownerService.getOwner(pet.ownerId!)])),
|
||||
takeUntilDestroyed(this.destroyRef)
|
||||
).subscribe({
|
||||
next: ([visit, pet, owner]) => { this.form.patchValue(visit); this.visit = visit; this.currentPet = pet; this.currentOwner = owner },
|
||||
error: error => this.snackBar.open(error)
|
||||
});
|
||||
}
|
||||
|
||||
onSubmit() {
|
||||
const formDate = this.form.controls['date'].value as DateTime|string;
|
||||
const visit = {
|
||||
date: typeof formDate === 'string' ? formDate : formDate.toISODate()!,
|
||||
description: this.form.controls['description'].value
|
||||
} as Visit;
|
||||
this.visitService.updateVisit(this.visit!.id, visit).subscribe({
|
||||
next: _ => this.router.navigate(['/owners', this.currentOwner!.id!]),
|
||||
error: error => this.snackBar.open(error)
|
||||
});
|
||||
}
|
||||
}
|
15
angular-client/src/app/visit-list/visit-list.component.html
Normal file
15
angular-client/src/app/visit-list/visit-list.component.html
Normal file
|
@ -0,0 +1,15 @@
|
|||
@if (visits.length !== 0) {
|
||||
<table mat-table [dataSource]="visits">
|
||||
<mat-text-column name="date" headerText="Visit Date"></mat-text-column>
|
||||
<mat-text-column name="description"></mat-text-column>
|
||||
<ng-container matColumnDef="actions">
|
||||
<th mat-header-cell *matHeaderCellDef>Actions</th>
|
||||
<td mat-cell *matCellDef="let visit">
|
||||
<a mat-button routerLink="/visit/{{visit.id}}/edit">Edit Visit</a>
|
||||
<button mat-button (click)="deleteVisit(visit)">Delete Visit</button>
|
||||
</td>
|
||||
</ng-container>
|
||||
<tr mat-header-row *matHeaderRowDef="columnsToDisplay"></tr>
|
||||
<tr mat-row *matRowDef="let rowData; columns: columnsToDisplay"></tr>
|
||||
</table>
|
||||
}
|
|
@ -0,0 +1,23 @@
|
|||
import { ComponentFixture, TestBed } from '@angular/core/testing';
|
||||
|
||||
import { VisitListComponent } from './visit-list.component';
|
||||
|
||||
describe('VisitListComponent', () => {
|
||||
let component: VisitListComponent;
|
||||
let fixture: ComponentFixture<VisitListComponent>;
|
||||
|
||||
beforeEach(async () => {
|
||||
await TestBed.configureTestingModule({
|
||||
imports: [VisitListComponent]
|
||||
})
|
||||
.compileComponents();
|
||||
|
||||
fixture = TestBed.createComponent(VisitListComponent);
|
||||
component = fixture.componentInstance;
|
||||
fixture.detectChanges();
|
||||
});
|
||||
|
||||
it('should create', () => {
|
||||
expect(component).toBeTruthy();
|
||||
});
|
||||
});
|
25
angular-client/src/app/visit-list/visit-list.component.ts
Normal file
25
angular-client/src/app/visit-list/visit-list.component.ts
Normal file
|
@ -0,0 +1,25 @@
|
|||
import { Component, inject, Input } from '@angular/core';
|
||||
import { Visit, VisitService } from '../../../build/client';
|
||||
import { MatSnackBar } from '@angular/material/snack-bar';
|
||||
import { RouterLink } from '@angular/router';
|
||||
import { MatTableModule } from '@angular/material/table';
|
||||
import { MatButtonModule } from '@angular/material/button';
|
||||
|
||||
@Component({
|
||||
selector: 'app-visit-list',
|
||||
imports: [RouterLink, MatTableModule, MatButtonModule],
|
||||
templateUrl: './visit-list.component.html',
|
||||
styleUrl: './visit-list.component.scss'
|
||||
})
|
||||
export class VisitListComponent {
|
||||
@Input() visits = [] as Visit[];
|
||||
columnsToDisplay = ['date', 'description', 'actions'];
|
||||
private readonly visitService = inject(VisitService);
|
||||
private readonly snackBar = inject(MatSnackBar);
|
||||
deleteVisit(visit: Visit) {
|
||||
this.visitService.deleteVisit(visit.id).subscribe({
|
||||
next: _ => this.visits = this.visits.filter(v => v.id !== visit.id),
|
||||
error: error => this.snackBar.open(error)
|
||||
});
|
||||
}
|
||||
}
|
|
@ -1,5 +1,2 @@
|
|||
<mat-card>
|
||||
<mat-card-header><mat-card-title>Welcome to Petclinic</mat-card-title></mat-card-header>
|
||||
<mat-card-content>Welcome</mat-card-content>
|
||||
<img style="margin: 0 auto" src="./assets/images/pets.png" alt="pets logo" />
|
||||
</mat-card>
|
||||
<h2>Welcome to Petclinic</h2>
|
||||
<img style="margin: 0 auto" src="./assets/images/pets.png" alt="pets logo" />
|
|
@ -3,7 +3,7 @@ import { MatCardModule } from '@angular/material/card';
|
|||
|
||||
@Component({
|
||||
selector: 'app-welcome',
|
||||
imports: [MatCardModule],
|
||||
imports: [],
|
||||
templateUrl: './welcome.component.html',
|
||||
styleUrl: './welcome.component.scss'
|
||||
})
|
||||
|
|
Loading…
Add table
Reference in a new issue