schlüssel neuen zylindern zuordnen geht

This commit is contained in:
Bastian Wagner
2026-02-22 17:08:50 +01:00
parent 6797b73eb1
commit e5c590165c
13 changed files with 163 additions and 1338 deletions

1
api/package-lock.json generated
View File

@@ -5691,7 +5691,6 @@
"version": "2.3.3", "version": "2.3.3",
"resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz",
"integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==",
"dev": true,
"hasInstallScript": true, "hasInstallScript": true,
"optional": true, "optional": true,
"os": [ "os": [

View File

@@ -122,5 +122,8 @@
"@schematics/angular:resolver": { "@schematics/angular:resolver": {
"typeSeparator": "." "typeSeparator": "."
} }
},
"cli": {
"analytics": false
} }
} }

1293
client/package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -78,16 +78,11 @@ export class LostKeysComponent extends AgGridContainerComponent {
markAsFound(key: IKey) { markAsFound(key: IKey) {
this.dialog.open(LostKeyComponent, { data: key, autoFocus: false }).afterClosed().subscribe({ this.dialog.open(LostKeyComponent, { data: key, autoFocus: false }).afterClosed().subscribe({
next: (result) => { next: async (result) => {
if (result == "") { if (result == "") {
key.keyLost = null; key.keyLost = null;
this.api.updateKey(key).subscribe({ await this.api.updateKey(key);
next: () => { this.loadLostKeys();
this.toast.success('Schlüssel als gefunden markiert');
this.loadLostKeys();
this.api.refreshKeys();
}
});
this.dataChanged = true; this.dataChanged = true;
} }
} }

View File

@@ -0,0 +1,23 @@
<h2 mat-dialog-title>Mehrere Schließanlagen gewählt!</h2>
<mat-dialog-content>
<div class="warning-message">
<mat-icon>warning</mat-icon>
<p>
Der Schlüssel ist Zylindern in mehreren Schließanlagen zugeordnet!
</p>
<p class="additional-info">
<!-- Additional information -->
<small>Zum Korrigieren abbrechen, ansonsten speichern</small>
</p>
</div>
</mat-dialog-content>
<mat-dialog-actions align="end">
<button matButton [mat-dialog-close]="false">
<mat-icon>close</mat-icon>
Vorgang abbrechen
</button>
<button matButton="elevated" [mat-dialog-close]="true" class="btn-warning">
<mat-icon>check</mat-icon>
Schlüssel speichern
</button>
</mat-dialog-actions>

View File

@@ -0,0 +1,23 @@
import { ComponentFixture, TestBed } from '@angular/core/testing';
import { MultipleCylinderSystemsDialogComponent } from './multiple-cylinder-systems-dialog.component';
describe('MultipleCylinderSystemsDialogComponent', () => {
let component: MultipleCylinderSystemsDialogComponent;
let fixture: ComponentFixture<MultipleCylinderSystemsDialogComponent>;
beforeEach(async () => {
await TestBed.configureTestingModule({
imports: [MultipleCylinderSystemsDialogComponent]
})
.compileComponents();
fixture = TestBed.createComponent(MultipleCylinderSystemsDialogComponent);
component = fixture.componentInstance;
fixture.detectChanges();
});
it('should create', () => {
expect(component).toBeTruthy();
});
});

View File

@@ -0,0 +1,14 @@
import { Component } from '@angular/core';
import { MatButtonModule } from '@angular/material/button';
import { MatDialogModule } from '@angular/material/dialog';
import { MatIconModule } from '@angular/material/icon';
@Component({
selector: 'app-multiple-cylinder-systems-dialog',
imports: [MatDialogModule, MatButtonModule, MatIconModule],
templateUrl: './multiple-cylinder-systems-dialog.component.html',
styleUrl: './multiple-cylinder-systems-dialog.component.scss',
})
export class MultipleCylinderSystemsDialogComponent {
}

View File

@@ -11,6 +11,6 @@
</mat-dialog-content> </mat-dialog-content>
<mat-dialog-actions> <mat-dialog-actions>
<button matButton [mat-dialog-close]="null">Abbrechen</button> <button matButton (click)="close(false)">Abbrechen</button>
<button matButton [mat-dialog-close]="selectedCylinders">Übernehmen</button> <button matButton="elevated" (click)="close(true)" class="btn-primary" [disabled]="selectedCylinders.length < 1" >Übernehmen</button>
</mat-dialog-actions> </mat-dialog-actions>

View File

@@ -3,11 +3,12 @@ import { ApiService } from '../../../../shared/api.service';
import { ICylinder } from '../../../../model/interface/cylinder.interface'; import { ICylinder } from '../../../../model/interface/cylinder.interface';
import { HotToastService } from '@ngxpert/hot-toast'; import { HotToastService } from '@ngxpert/hot-toast';
import { AgGridAngular } from 'ag-grid-angular'; import { AgGridAngular } from 'ag-grid-angular';
import { GridApi, GridOptions, GridReadyEvent } from 'ag-grid-community'; import { GridApi, GridOptions, GridReadyEvent, IRowNode } from 'ag-grid-community';
import { AG_GRID_LOCALE_DE } from '@ag-grid-community/locale'; import { AG_GRID_LOCALE_DE } from '@ag-grid-community/locale';
import { MAT_DIALOG_DATA, MatDialogModule, MatDialogRef } from '@angular/material/dialog'; import { MAT_DIALOG_DATA, MatDialog, MatDialogModule, MatDialogRef } from '@angular/material/dialog';
import { MatButtonModule } from '@angular/material/button'; import { MatButtonModule } from '@angular/material/button';
import { AgGridContainerComponent } from '../../../../shared/ag-grid/components/ag-grid-container/ag-grid-container.component'; import { AgGridContainerComponent } from '../../../../shared/ag-grid/components/ag-grid-container/ag-grid-container.component';
import { MultipleCylinderSystemsDialogComponent } from './multiple-cylinder-systems-dialog/multiple-cylinder-systems-dialog.component';
@Component({ @Component({
selector: 'app-select-key-cylinder', selector: 'app-select-key-cylinder',
@@ -20,6 +21,8 @@ export class SelectKeyCylinderComponent extends AgGridContainerComponent {
readonly dialogRef = inject(MatDialogRef<SelectKeyCylinderComponent>); readonly dialogRef = inject(MatDialogRef<SelectKeyCylinderComponent>);
readonly cylinders = inject<ICylinder[]>(MAT_DIALOG_DATA); readonly cylinders = inject<ICylinder[]>(MAT_DIALOG_DATA);
private dialog = inject(MatDialog);
gridApi!: GridApi; gridApi!: GridApi;
gridOptions: GridOptions = { gridOptions: GridOptions = {
@@ -48,6 +51,46 @@ export class SelectKeyCylinderComponent extends AgGridContainerComponent {
} }
selectionChanged(): void { selectionChanged(): void {
this.selectedCylinders =this.gridApi?.getSelectedRows(); this.selectedCylinders = this.gridApi?.getSelectedRows();
if (this.selectedCylinders.length == 0 ) {
this.toast.info('Jeder Schlüssel muss mindestens einem Zylinder zugeordnet sein. Bitte Zylinder wählen.')
}
}
preselectCylinders(cylinders: ICylinder[]) {
this.gridApi.setGridOption('loading', true)
const nodesToSelect: IRowNode<ICylinder>[] = [];
this.gridApi.forEachNode(node => {
if (cylinders.some(c => c.id == node.data.id )) {
nodesToSelect.push(node);
}
})
this.gridApi.setNodesSelected({
nodes: nodesToSelect,
newValue: true
})
this.gridApi.setGridOption('loading', false)
}
async close(data = false) {
if (!data) { return this.dialogRef.close() }
const amountOfSystems = Array.from(new Set(this.selectedCylinders.map( c => c.system.id )));
if (amountOfSystems.length == 1) {
return this.dialogRef.close(this.selectedCylinders);
}
this.dialog.open(MultipleCylinderSystemsDialogComponent, {}).afterClosed().subscribe({
next: val => {
if (val) {
return this.dialogRef.close(this.selectedCylinders);
}
}
})
} }
} }

View File

@@ -1,7 +1,7 @@
import { Component, inject } from '@angular/core'; import { Component, inject } from '@angular/core';
import { AG_GRID_LOCALE_DE } from '@ag-grid-community/locale'; import { AG_GRID_LOCALE_DE } from '@ag-grid-community/locale';
import { AgGridAngular } from 'ag-grid-angular'; import { AgGridAngular } from 'ag-grid-angular';
import { GridOptions,GridApi, GridReadyEvent, CellEditingStoppedEvent, ICellEditorParams, FilterActionParams, FilterAction, themeQuartz, Theme, ThemeDefaultParams } from 'ag-grid-community'; import { GridOptions,GridApi, GridReadyEvent, CellEditingStoppedEvent, ICellEditorParams, FilterActionParams, FilterAction, themeQuartz, Theme, ThemeDefaultParams, AgGridEvent, CellClickedEvent, CellDoubleClickedEvent } from 'ag-grid-community';
import { DatePipe } from '@angular/common'; import { DatePipe } from '@angular/common';
import { ApiService } from '../../shared/api.service'; import { ApiService } from '../../shared/api.service';
import { IKey } from '../../model/interface/key.interface'; import { IKey } from '../../model/interface/key.interface';
@@ -54,7 +54,9 @@ export class KeysComponent extends AgGridContainerComponent {
valueFormatter: (data: any) => { return data; }, valueFormatter: (data: any) => { return data; },
cellRenderer: (data: any) => {return data.value?.map((m: ICylinder) => m.name).join(', ')}, cellRenderer: (data: any) => {return data.value?.map((m: ICylinder) => m.name).join(', ')},
tooltipValueGetter: (data: any) => data.value?.map((m: ICylinder) => m.name).join(','), tooltipValueGetter: (data: any) => data.value?.map((m: ICylinder) => m.name).join(','),
onCellDoubleClicked(event) {}, onCellDoubleClicked: (event) => {
this.openSelectCylinder(event)
},
cellEditorPopup: true, cellEditorPopup: true,
filterValueGetter: (params: any) => {return params.data.cylinder?.map((m: ICylinder) => m.name).join(', ')}, filterValueGetter: (params: any) => {return params.data.cylinder?.map((m: ICylinder) => m.name).join(', ')},
}, },
@@ -157,24 +159,13 @@ export class KeysComponent extends AgGridContainerComponent {
this.setFilterToParams(); this.setFilterToParams();
} }
cellEditEnd(event: CellEditingStoppedEvent) { async cellEditEnd(event: CellEditingStoppedEvent) {
const key: IKey = event.data; const key: IKey = event.data;
if (!event.valueChanged || event.newValue == event.oldValue) { return; } if (!event.valueChanged || event.newValue == event.oldValue) { return; }
this.gridApi.setGridOption("loading", true); this.gridApi.setGridOption("loading", true);
this.api.updateKey(key) await this.api.updateKey(key)
.pipe( this.gridApi.setGridOption("loading", false);
this.toast.observe({
loading: 'speichern...',
success: 'Änderungen gespeichert',
error: 'Änderungen konnten nicht gespeichert werden!'
})
).subscribe({
next: () => {this.gridApi.setGridOption("loading", false);},
error: () => {
this.loadKeys();
}
})
} }
openCreateKey() { openCreateKey() {
@@ -195,17 +186,33 @@ export class KeysComponent extends AgGridContainerComponent {
}) })
} }
openSelectCylinder(params: any) { async openSelectCylinder(event: CellDoubleClickedEvent) {
this.dialog.open(SelectKeyCylinderComponent, { const key: IKey = event.data;
this.gridApi.setGridOption("loading", true);
const cylinders = await this.api.refreshCylinders()
this.gridApi.setGridOption('loading', false)
const ref = this.dialog.open(SelectKeyCylinderComponent, {
data: cylinders,
maxHeight: "calc(100vh - 24px)",
maxWidth: "calc(100vw - 24px)", maxWidth: "calc(100vw - 24px)",
width: "30vw", width: "50vw",
minWidth: "200px", minWidth: "300px",
disableClose: true height: "70vh",
}).afterClosed().subscribe({ disableClose: true,
next: key => { });
console.log(key)
ref.afterOpened().subscribe({
next: () => {
ref.componentInstance.preselectCylinders(event.data.cylinder);
} }
}) })
ref.afterClosed().subscribe({
next: (cylinders: ICylinder[]) => {
if (cylinders == null) { return; }
key.cylinder = cylinders;
this.api.updateKey(key)
}
});
} }
openArchive() { openArchive() {

View File

@@ -105,8 +105,7 @@ export class AgKeyActionsComponent implements ICellRendererAngularComp {
} }
this.key.keyLost = n; this.key.keyLost = n;
this.params.api.refreshCells(); this.params.api.refreshCells();
await this.api.updateKey(this.key).subscribe(); await this.api.updateKey(this.key)
this.api.refreshKeys();
} }
} }
}) })

View File

@@ -76,8 +76,20 @@ export class ApiService {
return this.http.get<IKey[]>('api/key/lost') return this.http.get<IKey[]>('api/key/lost')
} }
updateKey(key: IKey): Observable<IKey> { updateKey(key: IKey): Promise<IKey | null> {
return this.http.put<IKey>('api/key', key); return new Promise<IKey | null>(resolve => {
this.http.put<IKey>('api/key', key).pipe(
this.toast.observe({
loading: `Speichere Schlüssel ${key.name}....`,
success: `Schlüssel ${key.name} gespeichert.`,
error: `Es ist ein Fehler aufgetreten!`
})
).subscribe({
next: (key: IKey) => resolve(key),
error: () => resolve(null),
complete: () => { this.refreshKeys(); }
})
})
} }
updateCylinder(cylinder: ICylinder): Promise<boolean> { updateCylinder(cylinder: ICylinder): Promise<boolean> {
@@ -275,8 +287,8 @@ export class ApiService {
* cylinders * cylinders
* @returns Promise wenn geladen * @returns Promise wenn geladen
*/ */
refreshCylinders(): Promise<void> { refreshCylinders(): Promise<ICylinder[]> {
return new Promise<void>(resolve => { return new Promise<ICylinder[]>(resolve => {
this.getCylinders().subscribe({ this.getCylinders().subscribe({
next: data => { next: data => {
this.cylinders.next(data); this.cylinders.next(data);
@@ -284,7 +296,7 @@ export class ApiService {
error: () => { error: () => {
this.toast.error('Fehler beim Laden der Zylinder') this.toast.error('Fehler beim Laden der Zylinder')
}, },
complete: () => resolve() complete: () => resolve(this.cylinders.value)
}) })
}) })
} }