From bf64103369b0c5f51b03838221f73464750bee53 Mon Sep 17 00:00:00 2001 From: Bastian Wagner Date: Thu, 2 Jan 2025 11:17:28 +0100 Subject: [PATCH] Multicylinders --- api/src/model/entitites/cylinder.entity.ts | 3 +- api/src/model/entitites/key.entity.ts | 7 ++- .../model/entitites/key_activity.entity.ts | 5 +- api/src/model/interface/key.interface.ts | 2 +- .../mocks/services/mock.hottoast.service.ts | 10 +++- .../modules/keys/create/create.component.html | 25 +++++---- .../modules/keys/create/create.component.ts | 28 +++++++++- .../select-key-cylinder.component.html | 15 ++++++ .../select-key-cylinder.component.scss | 0 .../select-key-cylinder.component.spec.ts | 37 +++++++++++++ .../select-key-cylinder.component.ts | 54 +++++++++++++++++++ client/src/app/modules/keys/keys.component.ts | 47 +++++++++++----- 12 files changed, 199 insertions(+), 34 deletions(-) create mode 100644 client/src/app/modules/keys/create/select-key-cylinder/select-key-cylinder.component.html create mode 100644 client/src/app/modules/keys/create/select-key-cylinder/select-key-cylinder.component.scss create mode 100644 client/src/app/modules/keys/create/select-key-cylinder/select-key-cylinder.component.spec.ts create mode 100644 client/src/app/modules/keys/create/select-key-cylinder/select-key-cylinder.component.ts diff --git a/api/src/model/entitites/cylinder.entity.ts b/api/src/model/entitites/cylinder.entity.ts index 063dc97..e65d91b 100644 --- a/api/src/model/entitites/cylinder.entity.ts +++ b/api/src/model/entitites/cylinder.entity.ts @@ -4,6 +4,7 @@ import { CreateDateColumn, DeleteDateColumn, Entity, + ManyToMany, ManyToOne, OneToMany, PrimaryGeneratedColumn, @@ -20,7 +21,7 @@ export class Cylinder { @Column({ nullable: false, unique: true }) name: string; - @OneToMany(() => Key, (key) => key.cylinder) + @ManyToMany(() => Key, (key) => key.cylinder) keys: Key[]; @ManyToOne(() => KeySystem, (sys) => sys.cylinders) diff --git a/api/src/model/entitites/key.entity.ts b/api/src/model/entitites/key.entity.ts index ab569cc..9a98efa 100644 --- a/api/src/model/entitites/key.entity.ts +++ b/api/src/model/entitites/key.entity.ts @@ -3,6 +3,8 @@ import { CreateDateColumn, DeleteDateColumn, Entity, + JoinTable, + ManyToMany, ManyToOne, PrimaryGeneratedColumn, UpdateDateColumn, @@ -28,8 +30,9 @@ export class Key implements IKey { @Column({ name: 'lost', default: false }) keyLost: boolean; - @ManyToOne(() => Cylinder, (cylinder) => cylinder.keys) - cylinder: Cylinder; + @ManyToMany(() => Cylinder, (cylinder) => cylinder.keys) + @JoinTable() + cylinder: Cylinder[]; @ManyToOne(() => Customer, (customer) => customer.keys) customer: Customer; diff --git a/api/src/model/entitites/key_activity.entity.ts b/api/src/model/entitites/key_activity.entity.ts index fac6d45..71ace39 100644 --- a/api/src/model/entitites/key_activity.entity.ts +++ b/api/src/model/entitites/key_activity.entity.ts @@ -2,6 +2,7 @@ import { Column, CreateDateColumn, Entity, + ManyToMany, ManyToOne, PrimaryGeneratedColumn, } from 'typeorm'; @@ -27,8 +28,8 @@ export class KeyActivity implements IKey { @Column({ name: 'handed_out', default: false }) handedOut: boolean; - @ManyToOne(() => Cylinder) - cylinder: Cylinder; + @ManyToMany(() => Cylinder) + cylinder: Cylinder[]; @ManyToOne(() => Customer) customer: Customer; diff --git a/api/src/model/interface/key.interface.ts b/api/src/model/interface/key.interface.ts index dbb30a3..dcf3f2f 100644 --- a/api/src/model/interface/key.interface.ts +++ b/api/src/model/interface/key.interface.ts @@ -5,7 +5,7 @@ export interface IKey { name: string; nr: string; handedOut: boolean; - cylinder: Cylinder; + cylinder: Cylinder[]; customer: Customer; createdAt: Date; } diff --git a/client/mocks/services/mock.hottoast.service.ts b/client/mocks/services/mock.hottoast.service.ts index c3601fa..3db2bb1 100644 --- a/client/mocks/services/mock.hottoast.service.ts +++ b/client/mocks/services/mock.hottoast.service.ts @@ -2,10 +2,16 @@ import { map } from "rxjs"; jest.mock('@ngxpert/hot-toast', () => ({ HotToastService: { - observe: jest.fn().mockImplementation(() => map(x => x)) + observe: jest.fn().mockImplementation(() => map(x => x)), + info: jest.fn(), + error: jest.fn(), + success: jest.fn(), }, })); export class MockHotToastService { - observe = jest.fn().mockImplementation(() => map(x => x)) + observe = jest.fn().mockImplementation(() => map(x => x)); + info = jest.fn(); + error = jest.fn(); + success = jest.fn(); }; \ No newline at end of file diff --git a/client/src/app/modules/keys/create/create.component.html b/client/src/app/modules/keys/create/create.component.html index 0bd1bfb..bac86b0 100644 --- a/client/src/app/modules/keys/create/create.component.html +++ b/client/src/app/modules/keys/create/create.component.html @@ -18,18 +18,23 @@ Nummer auf dem Schlüssel - - Schließzylinder - - @for (item of cylinders; track $index) { - {{ item.name }} - } - - Wo sperrt der Schlüssel? - +
+ + Schließzylinder + + @for (item of cylinders; track $index) { + {{ item.name }} + } + + Wo sperrt der Schlüssel? + + +
- + \ No newline at end of file diff --git a/client/src/app/modules/keys/create/create.component.ts b/client/src/app/modules/keys/create/create.component.ts index cabe021..76b7a22 100644 --- a/client/src/app/modules/keys/create/create.component.ts +++ b/client/src/app/modules/keys/create/create.component.ts @@ -1,7 +1,7 @@ import { Component, inject } from '@angular/core'; import { FormControl, FormGroup, FormsModule, ReactiveFormsModule, Validators } from '@angular/forms'; import { MatButtonModule } from '@angular/material/button'; -import { MatDialogModule, MatDialogRef } from '@angular/material/dialog'; +import { MatDialog, MatDialogModule, MatDialogRef } from '@angular/material/dialog'; import { ApiService } from '../../../shared/api.service'; import { MatAutocompleteModule } from '@angular/material/autocomplete'; import { MatFormFieldModule } from '@angular/material/form-field'; @@ -9,11 +9,13 @@ import { MatInputModule } from '@angular/material/input'; import { map, Observable, startWith } from 'rxjs'; import { MatSelectModule } from '@angular/material/select'; import { HotToastService } from '@ngxpert/hot-toast'; +import { SelectKeyCylinderComponent } from './select-key-cylinder/select-key-cylinder.component'; +import { MatIconModule } from '@angular/material/icon'; @Component({ selector: 'app-create', standalone: true, - imports: [MatDialogModule, MatButtonModule, ReactiveFormsModule, FormsModule, MatFormFieldModule, MatInputModule, MatSelectModule, MatDialogModule], + imports: [MatDialogModule, MatButtonModule, ReactiveFormsModule, FormsModule, MatFormFieldModule, MatInputModule, MatSelectModule, MatDialogModule, MatIconModule], templateUrl: './create.component.html', styleUrl: './create.component.scss' }) @@ -22,6 +24,7 @@ export class CreateKeyComponent { private api: ApiService = inject(ApiService); private toast: HotToastService = inject(HotToastService); readonly dialogRef = inject(MatDialogRef); + private readonly dialog = inject(MatDialog); createForm = new FormGroup({ name: new FormControl(null, Validators.required), @@ -57,6 +60,8 @@ export class CreateKeyComponent { } save() { + console.log(this.createForm.value) + this.api.createKey(this.createForm.value as any) .pipe( this.toast.observe({ @@ -72,4 +77,23 @@ export class CreateKeyComponent { } }) } + + openSelectMultipleCylinders() { + this.dialog.open(SelectKeyCylinderComponent, { + maxHeight: "calc(100vh - 24px)", + maxWidth: "calc(100vw - 24px)", + width: "50vw", + minWidth: "300px", + height: "70vh", + disableClose: true, + data: this.cylinders + }).afterClosed().subscribe({ + next: c => { + if (c) { + this.createForm.controls.cylinder.patchValue(c); + console.log(c); + } + } + }) + } } diff --git a/client/src/app/modules/keys/create/select-key-cylinder/select-key-cylinder.component.html b/client/src/app/modules/keys/create/select-key-cylinder/select-key-cylinder.component.html new file mode 100644 index 0000000..01d486e --- /dev/null +++ b/client/src/app/modules/keys/create/select-key-cylinder/select-key-cylinder.component.html @@ -0,0 +1,15 @@ + + +

Zylinder auswählen

+ + + + + + + + \ No newline at end of file diff --git a/client/src/app/modules/keys/create/select-key-cylinder/select-key-cylinder.component.scss b/client/src/app/modules/keys/create/select-key-cylinder/select-key-cylinder.component.scss new file mode 100644 index 0000000..e69de29 diff --git a/client/src/app/modules/keys/create/select-key-cylinder/select-key-cylinder.component.spec.ts b/client/src/app/modules/keys/create/select-key-cylinder/select-key-cylinder.component.spec.ts new file mode 100644 index 0000000..e11e42b --- /dev/null +++ b/client/src/app/modules/keys/create/select-key-cylinder/select-key-cylinder.component.spec.ts @@ -0,0 +1,37 @@ +import { ComponentFixture, TestBed } from '@angular/core/testing'; + +import { SelectKeyCylinderComponent } from './select-key-cylinder.component'; +import { HotToastService } from '@ngxpert/hot-toast'; +import { MAT_DIALOG_DATA, MatDialogRef } from '@angular/material/dialog'; +import { MockHotToastService } from '../../../../../../mocks/services/mock.hottoast.service'; + +describe('SelectKeyCylinderComponent', () => { + let component: SelectKeyCylinderComponent; + let fixture: ComponentFixture; + + const mockHotToastService = { + info: jest.fn(), + error: jest.fn(), + success: jest.fn(), + } + + beforeEach(async () => { + await TestBed.configureTestingModule({ + imports: [SelectKeyCylinderComponent], + providers: [ + { provide: HotToastService, useClass: MockHotToastService }, + { provide: MatDialogRef, useValue: {} }, + { provide: MAT_DIALOG_DATA, useValue: [] } + ] + }) + .compileComponents(); + + fixture = TestBed.createComponent(SelectKeyCylinderComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); +}); diff --git a/client/src/app/modules/keys/create/select-key-cylinder/select-key-cylinder.component.ts b/client/src/app/modules/keys/create/select-key-cylinder/select-key-cylinder.component.ts new file mode 100644 index 0000000..72e2188 --- /dev/null +++ b/client/src/app/modules/keys/create/select-key-cylinder/select-key-cylinder.component.ts @@ -0,0 +1,54 @@ +import { Component, inject } from '@angular/core'; +import { ApiService } from '../../../../shared/api.service'; +import { ICylinder } from '../../../../model/interface/cylinder.interface'; +import { HotToastService } from '@ngxpert/hot-toast'; +import { AgGridAngular } from 'ag-grid-angular'; +import { GridApi, GridOptions, GridReadyEvent } from 'ag-grid-community'; +import { AG_GRID_LOCALE_DE } from '@ag-grid-community/locale'; +import { MAT_DIALOG_DATA, MatDialogModule, MatDialogRef } from '@angular/material/dialog'; +import { MatButtonModule } from '@angular/material/button'; + +@Component({ + selector: 'app-select-key-cylinder', + standalone: true, + imports: [AgGridAngular, MatDialogModule, MatButtonModule], + templateUrl: './select-key-cylinder.component.html', + styleUrl: './select-key-cylinder.component.scss' +}) +export class SelectKeyCylinderComponent { + private toast: HotToastService = inject(HotToastService); + readonly dialogRef = inject(MatDialogRef); + readonly cylinders = inject(MAT_DIALOG_DATA); + + gridApi!: GridApi; + + gridOptions: GridOptions = { + localeText: AG_GRID_LOCALE_DE, + loading: false, + rowData: this.cylinders, + onSelectionChanged: (event) => { + this.selectionChanged(); + }, + rowSelection: 'multiple', + columnDefs: [ + // selected rows + { colId: 'selected', headerName: '', checkboxSelection: true, width: 40, headerCheckboxSelection: true, headerCheckboxSelectionFilteredOnly: true }, + { colId: 'name', field: 'name' , headerName: 'Name', flex: 1, editable: true, sort: 'asc', filter: true }, + ] + }; + + selectedCylinders: ICylinder[] = []; + + ngOnInit(): void { + console.log(this.toast) + this.toast.error('Wähle die Zylinder aus, die dem Schlüssel zugeordnet werden sollen.'); + } + + onGridReady(params: GridReadyEvent) { + this.gridApi = params.api; + } + + selectionChanged(): void { + this.selectedCylinders =this.gridApi?.getSelectedRows(); + } +} diff --git a/client/src/app/modules/keys/keys.component.ts b/client/src/app/modules/keys/keys.component.ts index 00a0ba7..89d09ca 100644 --- a/client/src/app/modules/keys/keys.component.ts +++ b/client/src/app/modules/keys/keys.component.ts @@ -14,6 +14,7 @@ import { MatIconModule } from '@angular/material/icon'; import { ArchiveComponent } from './components/archive/archive.component'; import { AgLoadingComponent } from '../../shared/ag-grid/components/ag-loading/ag-loading.component'; import { map, of } from 'rxjs'; +import { ICylinder } from '../../model/interface/cylinder.interface'; @Component({ selector: 'app-keys', @@ -40,18 +41,21 @@ export class KeysComponent { { colId: 'name', field: 'name' , headerName: 'Name', flex: 1, editable: true, sort: 'asc', filter: true }, { colId: 'nr', field: 'nr' , headerName: 'Schlüsselnummer', flex: 1, editable: true, filter: true }, - { colId: 'cylinder', field: 'cylinder' , headerName: 'Zylinder', flex: 1, editable: true, filter: true, cellRenderer: (data: any) => {return data.value?.name}, cellEditor: 'agSelectCellEditor', - cellEditorParams: () => { - return { - values: this.cylinders, - } - }, - valueFormatter: (val) => { - return val.value?.name + ` (${val.value?.system?.name})`; - }, + { colId: 'cylinder', field: 'cylinder' , headerName: 'Zylinder', flex: 1, editable: false, filter: true, cellRenderer: (data: any) => {return data.value?.map((m: ICylinder) => m.name).join(', ')}, cellEditor: 'agSelectCellEditor', + // cellEditorParams: () => { + // return { + // values: this.cylinders, + // } + // }, + // valueFormatter: (val) => { + // return val.value?.name + ` (${val.value?.system?.name})`; + // }, cellEditorPopup: false }, - { colId: 'system', field: 'cylinder.system' , headerName: 'Schließanlage', flex: 1, editable: false, filter: true, cellRenderer: (data: any) => {return data.value?.name} }, + { colId: 'system', field: 'cylinder' , headerName: 'Schließanlage', flex: 1, editable: false, filter: true, cellRenderer: (data: any) => { + const s = new Set(data.value?.map((m: ICylinder) => m.system?.name)); + return [...s].join(', ') + } }, { colId: 'customer', field: 'customer' , headerName: 'Kunde', flex: 1, editable: false, filter: true, cellRenderer: (data: any) => {return data.value?.name} }, { field: 'createdAt' @@ -92,15 +96,25 @@ export class KeysComponent { this.api.getCylinders().subscribe({ next: n => { this.cylinders = n; + }, + error: () => { + this.cylinders = []; } }) } loadKeys() { this.gridApi.setGridOption("loading", true); - this.api.getKeys().subscribe(res => { - this.gridApi.setGridOption("rowData", res); - this.gridApi.setGridOption("loading", false); + this.api.getKeys().subscribe({ + next: n => { + this.gridApi.setGridOption("rowData", n); + this.gridApi.setGridOption("loading", false); + }, + error: () => { + this.gridApi.setGridOption("loading", false); + // set error in grid + this.toast.error('Fehler beim Laden der Schlüssel') + } }) } @@ -131,7 +145,12 @@ export class KeysComponent { } openCreateKey() { - this.dialog.open(CreateKeyComponent).afterClosed().subscribe({ + this.dialog.open(CreateKeyComponent, { + maxWidth: "calc(100vw - 24px)", + width: "30vw", + minWidth: "200px", + disableClose: true + }).afterClosed().subscribe({ next: key => { if (key) { let d = [...this.gridApi.getGridOption("rowData") || [], key];