1
0
Fork 0

All owner endpoints finished

This commit is contained in:
Chris Dombroski 2024-11-29 21:59:20 -05:00
parent 60dbcc3927
commit d0c40d51cd
45 changed files with 968 additions and 28 deletions

View file

@ -34,7 +34,8 @@
"styles": [
"src/styles.scss"
],
"scripts": []
"scripts": [],
"statsJson": true
},
"configurations": {
"production": {

View file

@ -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",

View file

@ -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",

View file

@ -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()]
};

View file

@ -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 }
];

View file

@ -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>

View file

@ -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();
});
});

View file

@ -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)
});
}
}

View 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>

View file

@ -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();
});
});

View 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)
});
}
}

View file

@ -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>
<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" />
</mat-card>

View file

@ -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'
})

View 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>

View 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();
});
});

View 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)
});
}
}

View 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>

View 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();
});
});

View 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)
});
}
}

View 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>
}

View 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();
});
});

View 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)
});
}
}

View 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 }
];

View 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 },
];

View 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 }
];

View 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}
];

View 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>
}

View 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();
});
});

View 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)
});
}
}

View 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>
}

View file

@ -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();
});
});

View 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)
});
}
}

View 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>
}

View file

@ -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();
});
});

View 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)
});
}
}

View file

@ -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>
<h2>Welcome to Petclinic</h2>
<img style="margin: 0 auto" src="./assets/images/pets.png" alt="pets logo" />
</mat-card>

View file

@ -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'
})