diff --git a/angular-client/angular.json b/angular-client/angular.json
index d595dc8..8e5cad1 100644
--- a/angular-client/angular.json
+++ b/angular-client/angular.json
@@ -34,7 +34,8 @@
"styles": [
"src/styles.scss"
],
- "scripts": []
+ "scripts": [],
+ "statsJson": true
},
"configurations": {
"production": {
diff --git a/angular-client/package-lock.json b/angular-client/package-lock.json
index b43a529..a875475 100644
--- a/angular-client/package-lock.json
+++ b/angular-client/package-lock.json
@@ -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",
diff --git a/angular-client/package.json b/angular-client/package.json
index da022c8..9d7339d 100644
--- a/angular-client/package.json
+++ b/angular-client/package.json
@@ -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",
diff --git a/angular-client/src/app/app.config.ts b/angular-client/src/app/app.config.ts
index aab26ad..148bc65 100644
--- a/angular-client/src/app/app.config.ts
+++ b/angular-client/src/app/app.config.ts
@@ -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()]
};
diff --git a/angular-client/src/app/app.routes.ts b/angular-client/src/app/app.routes.ts
deleted file mode 100644
index 92bb2da..0000000
--- a/angular-client/src/app/app.routes.ts
+++ /dev/null
@@ -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 }
-];
diff --git a/angular-client/src/app/owner-detail/owner-detail.component.html b/angular-client/src/app/owner-detail/owner-detail.component.html
new file mode 100644
index 0000000..9ee55e3
--- /dev/null
+++ b/angular-client/src/app/owner-detail/owner-detail.component.html
@@ -0,0 +1,32 @@
+
Owner Information
+
+
+ Name |
+ {{owner.firstName}} {{owner.lastName}} |
+
+
+ Address |
+ {{owner.address}} |
+
+
+ City |
+ {{owner.city}} |
+
+
+ Telephone |
+ {{owner.telephone}} |
+
+
+
+Back
+Edit Owner
+Add Pet
+
+Pets and Visits
+
+ @for(pet of owner.pets; track pet.id) {
+
+ |
+
+ }
+
\ No newline at end of file
diff --git a/angular-client/src/app/owner-detail/owner-detail.component.scss b/angular-client/src/app/owner-detail/owner-detail.component.scss
new file mode 100644
index 0000000..e69de29
diff --git a/angular-client/src/app/owner-detail/owner-detail.component.spec.ts b/angular-client/src/app/owner-detail/owner-detail.component.spec.ts
new file mode 100644
index 0000000..53eaf64
--- /dev/null
+++ b/angular-client/src/app/owner-detail/owner-detail.component.spec.ts
@@ -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;
+
+ beforeEach(async () => {
+ await TestBed.configureTestingModule({
+ imports: [OwnerDetailComponent]
+ })
+ .compileComponents();
+
+ fixture = TestBed.createComponent(OwnerDetailComponent);
+ component = fixture.componentInstance;
+ fixture.detectChanges();
+ });
+
+ it('should create', () => {
+ expect(component).toBeTruthy();
+ });
+});
diff --git a/angular-client/src/app/owner-detail/owner-detail.component.ts b/angular-client/src/app/owner-detail/owner-detail.component.ts
new file mode 100644
index 0000000..a37cacc
--- /dev/null
+++ b/angular-client/src/app/owner-detail/owner-detail.component.ts
@@ -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)
+ });
+ }
+
+}
diff --git a/angular-client/src/app/owner-edit/owner-edit.component.html b/angular-client/src/app/owner-edit/owner-edit.component.html
new file mode 100644
index 0000000..7be9d79
--- /dev/null
+++ b/angular-client/src/app/owner-edit/owner-edit.component.html
@@ -0,0 +1,55 @@
+Edit Owner
+
\ No newline at end of file
diff --git a/angular-client/src/app/owner-edit/owner-edit.component.scss b/angular-client/src/app/owner-edit/owner-edit.component.scss
new file mode 100644
index 0000000..e69de29
diff --git a/angular-client/src/app/owner-edit/owner-edit.component.spec.ts b/angular-client/src/app/owner-edit/owner-edit.component.spec.ts
new file mode 100644
index 0000000..1bc4b60
--- /dev/null
+++ b/angular-client/src/app/owner-edit/owner-edit.component.spec.ts
@@ -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;
+
+ beforeEach(async () => {
+ await TestBed.configureTestingModule({
+ imports: [OwnerEditComponent]
+ })
+ .compileComponents();
+
+ fixture = TestBed.createComponent(OwnerEditComponent);
+ component = fixture.componentInstance;
+ fixture.detectChanges();
+ });
+
+ it('should create', () => {
+ expect(component).toBeTruthy();
+ });
+});
diff --git a/angular-client/src/app/owner-edit/owner-edit.component.ts b/angular-client/src/app/owner-edit/owner-edit.component.ts
new file mode 100644
index 0000000..a2b5e68
--- /dev/null
+++ b/angular-client/src/app/owner-edit/owner-edit.component.ts
@@ -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)
+ });
+ }
+}
diff --git a/angular-client/src/app/page-not-found/page-not-found.component.html b/angular-client/src/app/page-not-found/page-not-found.component.html
index c683218..fdd0694 100644
--- a/angular-client/src/app/page-not-found/page-not-found.component.html
+++ b/angular-client/src/app/page-not-found/page-not-found.component.html
@@ -1,5 +1,3 @@
-
- Oops! Page not found !
- Not Found - 404 error
-
-
\ No newline at end of file
+Oops! Page not found !
+Not Found - 404 error
+
\ No newline at end of file
diff --git a/angular-client/src/app/page-not-found/page-not-found.component.ts b/angular-client/src/app/page-not-found/page-not-found.component.ts
index 4783a40..947606f 100644
--- a/angular-client/src/app/page-not-found/page-not-found.component.ts
+++ b/angular-client/src/app/page-not-found/page-not-found.component.ts
@@ -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'
})
diff --git a/angular-client/src/app/pet-add/pet-add.component.html b/angular-client/src/app/pet-add/pet-add.component.html
new file mode 100644
index 0000000..e0800e3
--- /dev/null
+++ b/angular-client/src/app/pet-add/pet-add.component.html
@@ -0,0 +1,35 @@
+Add Pet
+
\ No newline at end of file
diff --git a/angular-client/src/app/pet-add/pet-add.component.scss b/angular-client/src/app/pet-add/pet-add.component.scss
new file mode 100644
index 0000000..e69de29
diff --git a/angular-client/src/app/pet-add/pet-add.component.spec.ts b/angular-client/src/app/pet-add/pet-add.component.spec.ts
new file mode 100644
index 0000000..7da961b
--- /dev/null
+++ b/angular-client/src/app/pet-add/pet-add.component.spec.ts
@@ -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;
+
+ beforeEach(async () => {
+ await TestBed.configureTestingModule({
+ imports: [PetAddComponent]
+ })
+ .compileComponents();
+
+ fixture = TestBed.createComponent(PetAddComponent);
+ component = fixture.componentInstance;
+ fixture.detectChanges();
+ });
+
+ it('should create', () => {
+ expect(component).toBeTruthy();
+ });
+});
diff --git a/angular-client/src/app/pet-add/pet-add.component.ts b/angular-client/src/app/pet-add/pet-add.component.ts
new file mode 100644
index 0000000..07d059d
--- /dev/null
+++ b/angular-client/src/app/pet-add/pet-add.component.ts
@@ -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)
+ });
+ }
+}
diff --git a/angular-client/src/app/pet-edit/pet-edit.component.html b/angular-client/src/app/pet-edit/pet-edit.component.html
new file mode 100644
index 0000000..33aa210
--- /dev/null
+++ b/angular-client/src/app/pet-edit/pet-edit.component.html
@@ -0,0 +1,40 @@
+Edit Pet
+
\ No newline at end of file
diff --git a/angular-client/src/app/pet-edit/pet-edit.component.scss b/angular-client/src/app/pet-edit/pet-edit.component.scss
new file mode 100644
index 0000000..e69de29
diff --git a/angular-client/src/app/pet-edit/pet-edit.component.spec.ts b/angular-client/src/app/pet-edit/pet-edit.component.spec.ts
new file mode 100644
index 0000000..9248d99
--- /dev/null
+++ b/angular-client/src/app/pet-edit/pet-edit.component.spec.ts
@@ -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;
+
+ beforeEach(async () => {
+ await TestBed.configureTestingModule({
+ imports: [PetEditComponent]
+ })
+ .compileComponents();
+
+ fixture = TestBed.createComponent(PetEditComponent);
+ component = fixture.componentInstance;
+ fixture.detectChanges();
+ });
+
+ it('should create', () => {
+ expect(component).toBeTruthy();
+ });
+});
diff --git a/angular-client/src/app/pet-edit/pet-edit.component.ts b/angular-client/src/app/pet-edit/pet-edit.component.ts
new file mode 100644
index 0000000..f4cbd64
--- /dev/null
+++ b/angular-client/src/app/pet-edit/pet-edit.component.ts
@@ -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)
+ });
+ }
+
+}
diff --git a/angular-client/src/app/pet-list/pet-list.component.html b/angular-client/src/app/pet-list/pet-list.component.html
new file mode 100644
index 0000000..ae71283
--- /dev/null
+++ b/angular-client/src/app/pet-list/pet-list.component.html
@@ -0,0 +1,22 @@
+@if(pet != null) {
+
+
+
+
+ - Name
+ - {{pet.name}}
+ - Birth Date
+ - {{pet.birthDate}}
+ - Type
+ - {{pet.type.name}}
+ Edit Pet
+
+ Add Visit
+
+ |
+
+
+ |
+
+
+}
\ No newline at end of file
diff --git a/angular-client/src/app/pet-list/pet-list.component.scss b/angular-client/src/app/pet-list/pet-list.component.scss
new file mode 100644
index 0000000..e69de29
diff --git a/angular-client/src/app/pet-list/pet-list.component.spec.ts b/angular-client/src/app/pet-list/pet-list.component.spec.ts
new file mode 100644
index 0000000..53c91cf
--- /dev/null
+++ b/angular-client/src/app/pet-list/pet-list.component.spec.ts
@@ -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;
+
+ beforeEach(async () => {
+ await TestBed.configureTestingModule({
+ imports: [PetListComponent]
+ })
+ .compileComponents();
+
+ fixture = TestBed.createComponent(PetListComponent);
+ component = fixture.componentInstance;
+ fixture.detectChanges();
+ });
+
+ it('should create', () => {
+ expect(component).toBeTruthy();
+ });
+});
diff --git a/angular-client/src/app/pet-list/pet-list.component.ts b/angular-client/src/app/pet-list/pet-list.component.ts
new file mode 100644
index 0000000..5845b25
--- /dev/null
+++ b/angular-client/src/app/pet-list/pet-list.component.ts
@@ -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)
+ });
+ }
+}
diff --git a/angular-client/src/app/routes/app.routes.ts b/angular-client/src/app/routes/app.routes.ts
new file mode 100644
index 0000000..41f28c5
--- /dev/null
+++ b/angular-client/src/app/routes/app.routes.ts
@@ -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 }
+];
diff --git a/angular-client/src/app/routes/owner.routes.ts b/angular-client/src/app/routes/owner.routes.ts
new file mode 100644
index 0000000..7714c50
--- /dev/null
+++ b/angular-client/src/app/routes/owner.routes.ts
@@ -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 },
+];
\ No newline at end of file
diff --git a/angular-client/src/app/routes/pet.routes.ts b/angular-client/src/app/routes/pet.routes.ts
new file mode 100644
index 0000000..0436d28
--- /dev/null
+++ b/angular-client/src/app/routes/pet.routes.ts
@@ -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 }
+];
\ No newline at end of file
diff --git a/angular-client/src/app/routes/visit.routes.ts b/angular-client/src/app/routes/visit.routes.ts
new file mode 100644
index 0000000..d4ce568
--- /dev/null
+++ b/angular-client/src/app/routes/visit.routes.ts
@@ -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}
+];
\ No newline at end of file
diff --git a/angular-client/src/app/visit-add/visit-add.component.html b/angular-client/src/app/visit-add/visit-add.component.html
new file mode 100644
index 0000000..50395c5
--- /dev/null
+++ b/angular-client/src/app/visit-add/visit-add.component.html
@@ -0,0 +1,49 @@
+New Visit
+@if(currentPet && currentOwner) {
+Pet
+
+
+
+ Name |
+ Birth Date |
+ Type |
+ Owner |
+
+
+
+ {{currentPet.name}} |
+ {{currentPet.birthDate}} |
+ {{currentPet.type.name}} |
+ {{currentOwner.firstName}} {{currentOwner.lastName}} |
+
+
+
+
+
+
+Previous Visits
+
+
+}
\ No newline at end of file
diff --git a/angular-client/src/app/visit-add/visit-add.component.scss b/angular-client/src/app/visit-add/visit-add.component.scss
new file mode 100644
index 0000000..e69de29
diff --git a/angular-client/src/app/visit-add/visit-add.component.spec.ts b/angular-client/src/app/visit-add/visit-add.component.spec.ts
new file mode 100644
index 0000000..bd12e5e
--- /dev/null
+++ b/angular-client/src/app/visit-add/visit-add.component.spec.ts
@@ -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;
+
+ beforeEach(async () => {
+ await TestBed.configureTestingModule({
+ imports: [VisitAddComponent]
+ })
+ .compileComponents();
+
+ fixture = TestBed.createComponent(VisitAddComponent);
+ component = fixture.componentInstance;
+ fixture.detectChanges();
+ });
+
+ it('should create', () => {
+ expect(component).toBeTruthy();
+ });
+});
diff --git a/angular-client/src/app/visit-add/visit-add.component.ts b/angular-client/src/app/visit-add/visit-add.component.ts
new file mode 100644
index 0000000..66232a3
--- /dev/null
+++ b/angular-client/src/app/visit-add/visit-add.component.ts
@@ -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)
+ });
+ }
+}
diff --git a/angular-client/src/app/visit-edit/visit-edit.component.html b/angular-client/src/app/visit-edit/visit-edit.component.html
new file mode 100644
index 0000000..0d795f9
--- /dev/null
+++ b/angular-client/src/app/visit-edit/visit-edit.component.html
@@ -0,0 +1,44 @@
+Edit Visit
+@if(currentPet && currentOwner) {
+Pet
+
+
+
+ Name |
+ Birth Date |
+ Type |
+ Owner |
+
+
+
+ {{currentPet.name}} |
+ {{currentPet.birthDate}} |
+ {{currentPet.type.name}} |
+ {{currentOwner.firstName}} {{currentOwner.lastName}} |
+
+
+
+
+}
\ No newline at end of file
diff --git a/angular-client/src/app/visit-edit/visit-edit.component.scss b/angular-client/src/app/visit-edit/visit-edit.component.scss
new file mode 100644
index 0000000..e69de29
diff --git a/angular-client/src/app/visit-edit/visit-edit.component.spec.ts b/angular-client/src/app/visit-edit/visit-edit.component.spec.ts
new file mode 100644
index 0000000..235c664
--- /dev/null
+++ b/angular-client/src/app/visit-edit/visit-edit.component.spec.ts
@@ -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;
+
+ beforeEach(async () => {
+ await TestBed.configureTestingModule({
+ imports: [VisitEditComponent]
+ })
+ .compileComponents();
+
+ fixture = TestBed.createComponent(VisitEditComponent);
+ component = fixture.componentInstance;
+ fixture.detectChanges();
+ });
+
+ it('should create', () => {
+ expect(component).toBeTruthy();
+ });
+});
diff --git a/angular-client/src/app/visit-edit/visit-edit.component.ts b/angular-client/src/app/visit-edit/visit-edit.component.ts
new file mode 100644
index 0000000..57c8409
--- /dev/null
+++ b/angular-client/src/app/visit-edit/visit-edit.component.ts
@@ -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)
+ });
+ }
+}
diff --git a/angular-client/src/app/visit-list/visit-list.component.html b/angular-client/src/app/visit-list/visit-list.component.html
new file mode 100644
index 0000000..1773121
--- /dev/null
+++ b/angular-client/src/app/visit-list/visit-list.component.html
@@ -0,0 +1,15 @@
+@if (visits.length !== 0) {
+
+
+
+
+ Actions |
+
+ Edit Visit
+
+ |
+
+
+
+
+}
\ No newline at end of file
diff --git a/angular-client/src/app/visit-list/visit-list.component.scss b/angular-client/src/app/visit-list/visit-list.component.scss
new file mode 100644
index 0000000..e69de29
diff --git a/angular-client/src/app/visit-list/visit-list.component.spec.ts b/angular-client/src/app/visit-list/visit-list.component.spec.ts
new file mode 100644
index 0000000..49b366d
--- /dev/null
+++ b/angular-client/src/app/visit-list/visit-list.component.spec.ts
@@ -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;
+
+ beforeEach(async () => {
+ await TestBed.configureTestingModule({
+ imports: [VisitListComponent]
+ })
+ .compileComponents();
+
+ fixture = TestBed.createComponent(VisitListComponent);
+ component = fixture.componentInstance;
+ fixture.detectChanges();
+ });
+
+ it('should create', () => {
+ expect(component).toBeTruthy();
+ });
+});
diff --git a/angular-client/src/app/visit-list/visit-list.component.ts b/angular-client/src/app/visit-list/visit-list.component.ts
new file mode 100644
index 0000000..21d7c57
--- /dev/null
+++ b/angular-client/src/app/visit-list/visit-list.component.ts
@@ -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)
+ });
+ }
+}
diff --git a/angular-client/src/app/welcome/welcome.component.html b/angular-client/src/app/welcome/welcome.component.html
index a234aee..5fdfbdd 100644
--- a/angular-client/src/app/welcome/welcome.component.html
+++ b/angular-client/src/app/welcome/welcome.component.html
@@ -1,5 +1,2 @@
-
- Welcome to Petclinic
- Welcome
-
-
\ No newline at end of file
+Welcome to Petclinic
+
\ No newline at end of file
diff --git a/angular-client/src/app/welcome/welcome.component.ts b/angular-client/src/app/welcome/welcome.component.ts
index 304b2c0..5641919 100644
--- a/angular-client/src/app/welcome/welcome.component.ts
+++ b/angular-client/src/app/welcome/welcome.component.ts
@@ -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'
})