This commit is contained in:
Bastian Wagner
2026-02-19 22:29:46 +01:00
parent 4df51e0698
commit 29bfffc505
15 changed files with 107 additions and 21 deletions

View File

@@ -6,7 +6,6 @@ import {
Entity, Entity,
ManyToMany, ManyToMany,
ManyToOne, ManyToOne,
OneToMany,
PrimaryGeneratedColumn, PrimaryGeneratedColumn,
UpdateDateColumn, UpdateDateColumn,
} from 'typeorm'; } from 'typeorm';
@@ -18,9 +17,12 @@ export class Cylinder {
@PrimaryGeneratedColumn('uuid') @PrimaryGeneratedColumn('uuid')
id: string; id: string;
@Column({ nullable: false, unique: true }) @Column({ nullable: false })
name: string; name: string;
@Column({ name:'description', type: 'text', nullable: true })
description: string;
@ManyToMany(() => Key, (key) => key.cylinder, { onDelete: 'NO ACTION'}) @ManyToMany(() => Key, (key) => key.cylinder, { onDelete: 'NO ACTION'})
keys: Key[]; keys: Key[];

View File

@@ -60,6 +60,7 @@ export class CylinderService {
} }
async createCylinder(user: User, cylinder: Partial<Cylinder>) { async createCylinder(user: User, cylinder: Partial<Cylinder>) {
try {
const c = await this.cylinderRepo.save(this.cylinderRepo.create(cylinder)); const c = await this.cylinderRepo.save(this.cylinderRepo.create(cylinder));
this.systemActivityRepo.save({ this.systemActivityRepo.save({
@@ -68,6 +69,10 @@ export class CylinderService {
system: (c as any).system system: (c as any).system
}); });
return c return c
} catch (e) {
// this.log.log()
throw new HttpException('Zylinder konnte nicht angelegt werden', HttpStatus.BAD_REQUEST)
}
} }
getDeletedCylinders(user: User) { getDeletedCylinders(user: User) {

View File

@@ -8,6 +8,7 @@ import {
Delete, Delete,
Req, Req,
UseGuards, UseGuards,
Put,
} from '@nestjs/common'; } from '@nestjs/common';
import { SystemService } from './system.service'; import { SystemService } from './system.service';
import { CreateSystemDto } from './dto/create-system.dto'; import { CreateSystemDto } from './dto/create-system.dto';
@@ -47,7 +48,7 @@ export class SystemController {
return this.systemService.findOne(id); return this.systemService.findOne(id);
} }
@Patch(':id') @Put(':id')
update(@Param('id') id: string, @Body() updateSystemDto: UpdateSystemDto) { update(@Param('id') id: string, @Body() updateSystemDto: UpdateSystemDto) {
return this.systemService.update(id, updateSystemDto); return this.systemService.update(id, updateSystemDto);
} }

View File

@@ -3,6 +3,7 @@ import { IKey } from "./key.interface";
export interface ICylinder { export interface ICylinder {
id: string; id: string;
name: string; name: string;
description: string;
createdAt: string; createdAt: string;
updatedAt: string; updatedAt: string;
deletedAt: string; deletedAt: string;

View File

@@ -12,6 +12,12 @@
} }
</mat-form-field> </mat-form-field>
<mat-form-field>
<mat-label>Beschreibung</mat-label>
<input type="text" matInput formControlName="description" maxlength="255">
<mat-hint>Zylinderlänge und co.</mat-hint>
</mat-form-field>
<mat-form-field> <mat-form-field>
<mat-label>Schließanlage</mat-label> <mat-label>Schließanlage</mat-label>
<mat-select formControlName="system"> <mat-select formControlName="system">

View File

@@ -25,11 +25,12 @@ export class CreateCylinderComponent {
createForm = new FormGroup({ createForm = new FormGroup({
name: new FormControl<string | null>(null, Validators.required), name: new FormControl<string | null>(null, Validators.required),
system: new FormControl<any>(null, Validators.required) system: new FormControl<any>(null, Validators.required),
description: new FormControl<string | null>(null)
}); });
ngOnInit() { ngOnInit() {
this.api.getSystems().subscribe({ this.api.systems.asObservable().subscribe({
next: systems => { next: systems => {
this.systems = systems; this.systems = systems;
} }

View File

@@ -1,6 +1,6 @@
import { Component, inject } from '@angular/core'; import { Component, inject } from '@angular/core';
import { HELPER } from '../../shared/helper.service'; import { HELPER } from '../../shared/helper.service';
import { GridApi, GridOptions, GridReadyEvent } from 'ag-grid-community'; import { CellEditingStoppedEvent, GridApi, GridOptions, GridReadyEvent } from 'ag-grid-community';
import { AgGridAngular } from 'ag-grid-angular'; import { AgGridAngular } from 'ag-grid-angular';
import { ApiService } from '../../shared/api.service'; import { ApiService } from '../../shared/api.service';
import { DatePipe } from '@angular/common'; import { DatePipe } from '@angular/common';
@@ -11,6 +11,7 @@ import { MatIconModule } from '@angular/material/icon';
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 { CylinderArchiveComponent } from './components/cylinder-archive/cylinder-archive.component'; import { CylinderArchiveComponent } from './components/cylinder-archive/cylinder-archive.component';
import { ICylinder } from '../../model/interface/cylinder.interface';
@Component({ @Component({
selector: 'app-cylinder', selector: 'app-cylinder',
@@ -33,7 +34,8 @@ export class CylinderComponent extends AgGridContainerComponent {
super(); super();
this.gridOptions.columnDefs = [ this.gridOptions.columnDefs = [
{ field: 'name', headerName: 'Name', sort: 'asc', flex: 1, filter: true }, { field: 'name', headerName: 'Name', sort: 'asc', flex: 1, filter: true, editable: true },
{ field: 'description', headerName: 'Beschreibung', flex: 1, filter: true, editable: true },
{ field: 'system.name', headerName: 'System', flex: 1, filter: true }, { field: 'system.name', headerName: 'System', flex: 1, filter: true },
{ field: 'keyCount', headerName: 'Anzahl Schlüssel', flex: 0, type: 'number' }, { field: 'keyCount', headerName: 'Anzahl Schlüssel', flex: 0, type: 'number' },
{ field: 'createdAt', headerName: 'Angelegt', cellRenderer: (data: any) => data.value ? this.datePipe.transform(new Date(data.value)) : '-' }, { field: 'createdAt', headerName: 'Angelegt', cellRenderer: (data: any) => data.value ? this.datePipe.transform(new Date(data.value)) : '-' },
@@ -54,6 +56,7 @@ export class CylinderComponent extends AgGridContainerComponent {
onGridReady(params: GridReadyEvent) { onGridReady(params: GridReadyEvent) {
this.gridApi = params.api; this.gridApi = params.api;
this.gridApi.addEventListener("cellEditingStopped", evt => this.cellEditEnd(evt));
this.loadCylinders(); this.loadCylinders();
this.api.cylinders.asObservable().subscribe({ this.api.cylinders.asObservable().subscribe({
next: (data) => { next: (data) => {
@@ -63,6 +66,15 @@ export class CylinderComponent extends AgGridContainerComponent {
}) })
} }
private async cellEditEnd(event: CellEditingStoppedEvent) {
const cylinder: ICylinder = event.data;
if (!event.valueChanged || event.newValue == event.oldValue) { return; }
await this.api.updateCylinder(cylinder)
}
openCreateCylinder() { openCreateCylinder() {
this.dialog.open(CreateCylinderComponent, { this.dialog.open(CreateCylinderComponent, {
maxWidth: "calc(100vw - 24px)", maxWidth: "calc(100vw - 24px)",

View File

@@ -58,6 +58,7 @@
style="width: 100%; height: 100%;" style="width: 100%; height: 100%;"
(gridReady)="onGridReady($event)" (gridReady)="onGridReady($event)"
[gridOptions]="gridOptions!" [gridOptions]="gridOptions!"
[theme]="myTheme"
/> />
</div> </div>
</mat-dialog-content> </mat-dialog-content>

View File

@@ -25,6 +25,7 @@ import { GridApi, GridOptions, GridReadyEvent } 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 { AgGridAngular } from 'ag-grid-angular'; import { AgGridAngular } from 'ag-grid-angular';
import { MatIconModule } from '@angular/material/icon'; import { MatIconModule } from '@angular/material/icon';
import { AgGridContainerComponent } from '../../../../shared/ag-grid/components/ag-grid-container/ag-grid-container.component';
@Component({ @Component({
selector: 'app-handover-dialog', selector: 'app-handover-dialog',
@@ -38,7 +39,7 @@ import { MatIconModule } from '@angular/material/icon';
templateUrl: './handover-dialog.component.html', templateUrl: './handover-dialog.component.html',
styleUrl: './handover-dialog.component.scss' styleUrl: './handover-dialog.component.scss'
}) })
export class HandoverDialogComponent { export class HandoverDialogComponent extends AgGridContainerComponent {
private api: ApiService = inject(ApiService); private api: ApiService = inject(ApiService);
readonly dialogRef = inject(MatDialogRef<HandoverDialogComponent>); readonly dialogRef = inject(MatDialogRef<HandoverDialogComponent>);

View File

@@ -6,6 +6,7 @@
style="width: 100%; height: 100%;" style="width: 100%; height: 100%;"
(gridReady)="onGridReady($event)" (gridReady)="onGridReady($event)"
[gridOptions]="gridOptions!" [gridOptions]="gridOptions!"
[theme]="myTheme"
/> />
</mat-dialog-content> </mat-dialog-content>

View File

@@ -7,6 +7,7 @@ import { GridApi, GridOptions, GridReadyEvent } 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, 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';
@Component({ @Component({
selector: 'app-select-key-cylinder', selector: 'app-select-key-cylinder',
@@ -14,7 +15,7 @@ import { MatButtonModule } from '@angular/material/button';
templateUrl: './select-key-cylinder.component.html', templateUrl: './select-key-cylinder.component.html',
styleUrl: './select-key-cylinder.component.scss' styleUrl: './select-key-cylinder.component.scss'
}) })
export class SelectKeyCylinderComponent { export class SelectKeyCylinderComponent extends AgGridContainerComponent {
private toast: HotToastService = inject(HotToastService); private toast: HotToastService = inject(HotToastService);
readonly dialogRef = inject(MatDialogRef<SelectKeyCylinderComponent>); readonly dialogRef = inject(MatDialogRef<SelectKeyCylinderComponent>);
readonly cylinders = inject<ICylinder[]>(MAT_DIALOG_DATA); readonly cylinders = inject<ICylinder[]>(MAT_DIALOG_DATA);

View File

@@ -149,7 +149,6 @@ export class KeysComponent extends AgGridContainerComponent {
this.gridApi.addEventListener("cellEditingStopped", evt => this.cellEditEnd(evt)); this.gridApi.addEventListener("cellEditingStopped", evt => this.cellEditEnd(evt));
this.api.keys.asObservable().subscribe({ this.api.keys.asObservable().subscribe({
next: keys => { next: keys => {
console.log(keys)
this.gridApi.setGridOption("rowData", keys); this.gridApi.setGridOption("rowData", keys);
this.gridApi.setGridOption("loading", false); this.gridApi.setGridOption("loading", false);
} }

View File

@@ -30,7 +30,7 @@ export class SystemComponent extends AgGridContainerComponent {
super(); super();
this.gridOptions.columnDefs = [ this.gridOptions.columnDefs = [
{ colId: 'name', field: 'name', headerName: 'Name', sort: 'asc', flex: 1}, { colId: 'name', field: 'name', headerName: 'Name', sort: 'asc', flex: 1},
{ colId: 'cylinderCount', field: 'cylinders', headerName: 'Zylinderanzahl', flex: 0, cellRenderer: (data: any) => data.value.length}, { colId: 'cylinderCount', field: 'cylinders', headerName: 'Zylinderanzahl', flex: 0, cellRenderer: (data: any) => data.value?.length || 0},
{ field: 'createdAt', headerName: 'Angelegt', cellRenderer: (data: any) => data.value ? this.datePipe.transform(new Date(data.value)) : '-' }, { field: 'createdAt', headerName: 'Angelegt', cellRenderer: (data: any) => data.value ? this.datePipe.transform(new Date(data.value)) : '-' },
{ field: 'updatedAt', headerName: 'Upgedated', cellRenderer: (data: any) => data.value ? this.datePipe.transform(new Date(data.value)) : '-' }, { field: 'updatedAt', headerName: 'Upgedated', cellRenderer: (data: any) => data.value ? this.datePipe.transform(new Date(data.value)) : '-' },
{ {
@@ -43,17 +43,20 @@ export class SystemComponent extends AgGridContainerComponent {
]; ];
} }
loadSystems() { async loadSystems() {
this.api.getSystems().subscribe({ this.gridApi.setGridOption("loading", true);
next: n => { await this.api.refreshSystems();
this.gridApi.setGridOption("rowData", n);
this.gridApi.setGridOption("loading", false); this.gridApi.setGridOption("loading", false);
} }
})
}
onGridReady(params: GridReadyEvent) { onGridReady(params: GridReadyEvent) {
this.gridApi = params.api; this.gridApi = params.api;
this.api.systems.asObservable().subscribe({
next: systems => {
this.gridApi.setGridOption("rowData", systems);
this.gridApi.setGridOption("loading", false);
}
})
this.loadSystems(); this.loadSystems();
} }

View File

@@ -15,6 +15,7 @@ export class ApiService {
public keys: BehaviorSubject<IKey[]> = new BehaviorSubject<IKey[]>([]); public keys: BehaviorSubject<IKey[]> = new BehaviorSubject<IKey[]>([]);
public cylinders: BehaviorSubject<ICylinder[]> = new BehaviorSubject<ICylinder[]>([]); public cylinders: BehaviorSubject<ICylinder[]> = new BehaviorSubject<ICylinder[]>([]);
public systems: BehaviorSubject<any[]> = new BehaviorSubject<any[]>([]);
public user: BehaviorSubject<IUser> = new BehaviorSubject<IUser>(null!); public user: BehaviorSubject<IUser> = new BehaviorSubject<IUser>(null!);
@@ -73,6 +74,24 @@ export class ApiService {
return this.http.put<IKey>('api/key', key); return this.http.put<IKey>('api/key', key);
} }
updateCylinder(cylinder: ICylinder): Promise<boolean> {
return new Promise<boolean>(resolve => {
this.http.put('api/cylinder', cylinder).pipe(
this.toast.observe({
loading: `Zylinder ${cylinder} wird gespeichert...`,
success: `Zylinder ${cylinder.name} gespeichert.`,
error: 'Es ist ein Fehler aufgetreten'
})
).subscribe({
next: () => resolve(true),
error: () => resolve(false),
complete: () => {
this.refreshCylinders()
}
})
})
}
createKey(key: any) { createKey(key: any) {
return this.http.post<IKey>('api/key', key); return this.http.post<IKey>('api/key', key);
} }
@@ -81,7 +100,7 @@ export class ApiService {
return this.http.post('api/system', keySystem); return this.http.post('api/system', keySystem);
} }
getSystems(): Observable<any[]> { private getSystems(): Observable<any[]> {
return this.http.get<any[]>('api/system'); return this.http.get<any[]>('api/system');
} }
@@ -139,6 +158,24 @@ export class ApiService {
}) })
} }
deleteSystem(system: any): Promise<boolean> {
return new Promise<boolean>(resolve => {
this.http.delete(`api/system${system.id}`).pipe(
this.toast.observe({
loading: `Lösche Schließsystem ${system.name}...`,
success: `Schließsystem ${system.name} wurde gelöscht.`,
error: 'Es ist ein Fehler aufgetreten'
})
).subscribe({
next: () => resolve(true),
error: () => resolve(false),
complete: () => {
this.refreshSystems();
}
})
})
}
getKeyArchive(): Observable<IKey[]> { getKeyArchive(): Observable<IKey[]> {
return this.http.get<IKey[]>('api/key/archive'); return this.http.get<IKey[]>('api/key/archive');
} }
@@ -184,6 +221,21 @@ export class ApiService {
}) })
} }
refreshSystems(): Promise<void> {
return new Promise(resolve => {
this.getSystems().subscribe({
next: data => {
this.systems.next(data);
resolve()
},
error: () => {
this.toast.error('Fehler beim Laden der Schließsysteme')
},
complete: () => resolve()
})
})
}
deleteCylinder(cylinder: ICylinder): Promise<boolean> { deleteCylinder(cylinder: ICylinder): Promise<boolean> {
return new Promise<boolean>(resolve => { return new Promise<boolean>(resolve => {
this.http.delete(`api/cylinder/${cylinder.id}`).pipe( this.http.delete(`api/cylinder/${cylinder.id}`).pipe(

View File

@@ -216,6 +216,6 @@ div.ag-row {
} }
.ag-filter-body-wrapper { .ag-filter-body-wrapper {
padding: 8px; padding: 8px 12px;
gap: 6px; gap: 6px;
} }