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