From 6797b73eb1f6b4e5125a18540e650c231fcef32c Mon Sep 17 00:00:00 2001 From: Bastian Wagner Date: Fri, 20 Feb 2026 16:44:59 +0100 Subject: [PATCH] =?UTF-8?q?Schlie=C3=9Fanlagen=20k=C3=B6nnen=20gel=C3=B6sc?= =?UTF-8?q?ht=20werden?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- api/src/model/entitites/system.entity.ts | 4 ++ api/src/modules/system/system.controller.ts | 10 +++ api/src/modules/system/system.service.ts | 26 +++++++ .../model/interface/keysystem.interface.ts | 7 ++ .../system-archive.component.html | 15 ++++ .../system-archive.component.scss | 0 .../system-archive.component.spec.ts | 23 ++++++ .../system-archive.component.ts | 70 +++++++++++++++++++ .../app/modules/system/system.component.html | 1 + .../app/modules/system/system.component.ts | 18 ++++- .../ag-delete-system.component.html | 20 ++++++ .../ag-delete-system.component.scss | 0 .../ag-delete-system.component.spec.ts | 23 ++++++ .../ag-delete-system.component.ts | 17 +++++ .../ag-system-manager.component.html | 3 +- .../ag-system-manager.component.ts | 47 ++++++++++--- client/src/app/shared/api.service.ts | 32 ++++++++- 17 files changed, 302 insertions(+), 14 deletions(-) create mode 100644 client/src/app/model/interface/keysystem.interface.ts create mode 100644 client/src/app/modules/system/components/system-archive/system-archive.component.html create mode 100644 client/src/app/modules/system/components/system-archive/system-archive.component.scss create mode 100644 client/src/app/modules/system/components/system-archive/system-archive.component.spec.ts create mode 100644 client/src/app/modules/system/components/system-archive/system-archive.component.ts create mode 100644 client/src/app/shared/ag-grid/components/ag-delete-system/ag-delete-system.component.html create mode 100644 client/src/app/shared/ag-grid/components/ag-delete-system/ag-delete-system.component.scss create mode 100644 client/src/app/shared/ag-grid/components/ag-delete-system/ag-delete-system.component.spec.ts create mode 100644 client/src/app/shared/ag-grid/components/ag-delete-system/ag-delete-system.component.ts diff --git a/api/src/model/entitites/system.entity.ts b/api/src/model/entitites/system.entity.ts index 4d9fcda..ed288fe 100644 --- a/api/src/model/entitites/system.entity.ts +++ b/api/src/model/entitites/system.entity.ts @@ -1,6 +1,7 @@ import { Column, CreateDateColumn, + DeleteDateColumn, Entity, JoinTable, ManyToMany, @@ -32,4 +33,7 @@ export class KeySystem implements IKeySystem { @UpdateDateColumn({ name: 'updatet_at' }) updatedAt: Date; + + @DeleteDateColumn() + deletedAt: Date; } diff --git a/api/src/modules/system/system.controller.ts b/api/src/modules/system/system.controller.ts index 816d5ca..1967fe2 100644 --- a/api/src/modules/system/system.controller.ts +++ b/api/src/modules/system/system.controller.ts @@ -33,6 +33,11 @@ export class SystemController { findAll(@Req() req: AuthenticatedRequest) { return this.systemService.findAll(req.user); } + + @Get('archive') + findDeleted(@Req() req: AuthenticatedRequest) { + return this.systemService.findDeleted(req.user); + } @Get(':id/manager') getManagers(@Param('id') id: string) { @@ -57,4 +62,9 @@ export class SystemController { remove(@Param('id') id: string) { return this.systemService.remove(id); } + + @Put(':id/restore') + restoreKey(@Req() req: AuthenticatedRequest, @Param('id') id: string) { + return this.systemService.restore(req.user, id); + } } diff --git a/api/src/modules/system/system.service.ts b/api/src/modules/system/system.service.ts index ae1af35..372bc34 100644 --- a/api/src/modules/system/system.service.ts +++ b/api/src/modules/system/system.service.ts @@ -7,6 +7,7 @@ import { IUser } from 'src/model/interface'; import { MailService } from '../mail/mail.service'; import { ConfigService } from '@nestjs/config'; import { ActivityHelperService } from 'src/shared/service/activity.logger.service'; +import { IsNull, Not } from 'typeorm'; @Injectable() export class SystemService { @@ -56,6 +57,20 @@ export class SystemService { return systems; } + async findDeleted(user: User) { + let systems = await this.systemRepo.find({ + where: { managers: { id: user.id }, deletedAt: Not(IsNull()) }, + order: { name: { direction: 'ASC' } }, + withDeleted: true, + }); + + if (this.isDevelopMode) { + systems = systems.filter(s => s.name.toLocaleLowerCase().includes('develop')); + } + + return systems; + } + findOne(id: string) { return this.systemRepo.findOne({ where: { id: id } }); } @@ -136,4 +151,15 @@ export class SystemService { return sys.managers; } + + async restore(user: User, id: string) { + const key = await this.systemRepo.findOneOrFail({ + where: { id: id, managers: { id: user.id } }, + withDeleted: true, + }); + key.deletedAt = null; + // await this.activityService.logKeyRestored(user, key); + // await this.helper.deleteKeyArchiveCache(); + return this.systemRepo.save(key); + } } diff --git a/client/src/app/model/interface/keysystem.interface.ts b/client/src/app/model/interface/keysystem.interface.ts new file mode 100644 index 0000000..22a45d2 --- /dev/null +++ b/client/src/app/model/interface/keysystem.interface.ts @@ -0,0 +1,7 @@ +export interface ISystem { + id: string; + createdAt: string; + updatedAt: string; + deletedAt?: string; + name: string; +} \ No newline at end of file diff --git a/client/src/app/modules/system/components/system-archive/system-archive.component.html b/client/src/app/modules/system/components/system-archive/system-archive.component.html new file mode 100644 index 0000000..3ec5f58 --- /dev/null +++ b/client/src/app/modules/system/components/system-archive/system-archive.component.html @@ -0,0 +1,15 @@ +

Gelöschte Schließanlagen

+ + @if(myTheme) { + + } + + + + + \ No newline at end of file diff --git a/client/src/app/modules/system/components/system-archive/system-archive.component.scss b/client/src/app/modules/system/components/system-archive/system-archive.component.scss new file mode 100644 index 0000000..e69de29 diff --git a/client/src/app/modules/system/components/system-archive/system-archive.component.spec.ts b/client/src/app/modules/system/components/system-archive/system-archive.component.spec.ts new file mode 100644 index 0000000..d2cf062 --- /dev/null +++ b/client/src/app/modules/system/components/system-archive/system-archive.component.spec.ts @@ -0,0 +1,23 @@ +import { ComponentFixture, TestBed } from '@angular/core/testing'; + +import { SystemArchiveComponent } from './system-archive.component'; + +describe('SystemArchiveComponent', () => { + let component: SystemArchiveComponent; + let fixture: ComponentFixture; + + beforeEach(async () => { + await TestBed.configureTestingModule({ + imports: [SystemArchiveComponent] + }) + .compileComponents(); + + fixture = TestBed.createComponent(SystemArchiveComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); +}); diff --git a/client/src/app/modules/system/components/system-archive/system-archive.component.ts b/client/src/app/modules/system/components/system-archive/system-archive.component.ts new file mode 100644 index 0000000..99ce31f --- /dev/null +++ b/client/src/app/modules/system/components/system-archive/system-archive.component.ts @@ -0,0 +1,70 @@ +import { Component, inject, LOCALE_ID } from '@angular/core'; +import { AgGridContainerComponent } from '../../../../shared/ag-grid/components/ag-grid-container/ag-grid-container.component'; +import { DatePipe } from '@angular/common'; +import { MatButtonModule } from '@angular/material/button'; +import { MatDialogModule } from '@angular/material/dialog'; +import { MatIconModule } from '@angular/material/icon'; +import { AgGridAngular } from 'ag-grid-angular'; +import { GridApi, GridOptions, GridReadyEvent } from 'ag-grid-community'; +import { ApiService } from '../../../../shared/api.service'; +import { HELPER } from '../../../../shared/helper.service'; +import { ISystem } from '../../../../model/interface/keysystem.interface'; + +@Component({ + selector: 'app-system-archive', + imports: [MatDialogModule, AgGridAngular, MatButtonModule, MatIconModule], + templateUrl: './system-archive.component.html', + styleUrl: './system-archive.component.scss', + providers: [DatePipe, { provide: LOCALE_ID, useValue: 'de-DE' }] +}) +export class SystemArchiveComponent extends AgGridContainerComponent { + private api: ApiService = inject(ApiService); + private datePipe = inject(DatePipe); + + gridApi!: GridApi; + + gridOptions: GridOptions = HELPER.getGridOptions(); + + constructor() { + super(); + + this.gridOptions.columnDefs = [ + { 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 }, + { + field: 'deletedAt' + , headerName: 'Gelöscht' + , width: 160 + // , type: 'date' + , cellRenderer: (data: any) => this.datePipe.transform(new Date(data.value), 'short') + }, + { + width: 40, + cellRenderer: () => '
', + onCellClicked: (event) => { this.restoreSystem(event.data);}, + tooltipValueGetter: () => 'Wiederherstellen', + sortable: false + } + ]; + this.gridOptions.rowHeight = 36; + this.gridOptions.overlayNoRowsTemplate = 'Bisher wurden keine Schließanlagen gelöscht. Sobald dies der Fall ist, werden sie hier angezeigt.'; + } + + onGridReady(params: GridReadyEvent) { + this.gridApi = params.api; + this.loadSystems(); + } + + async loadSystems() { + this.gridApi.setGridOption("loading", true); + const systems = await this.api.getSystemArchive() + this.gridApi.setGridOption("rowData", systems); + this.gridApi.setGridOption("loading", false); + } + + async restoreSystem(system: ISystem) { + await this.api.restoreSystem(system); + this.loadSystems(); + } + +} diff --git a/client/src/app/modules/system/system.component.html b/client/src/app/modules/system/system.component.html index 1228c81..2b47100 100644 --- a/client/src/app/modules/system/system.component.html +++ b/client/src/app/modules/system/system.component.html @@ -8,4 +8,5 @@ }
+
\ No newline at end of file diff --git a/client/src/app/modules/system/system.component.ts b/client/src/app/modules/system/system.component.ts index d41477c..e9368a6 100644 --- a/client/src/app/modules/system/system.component.ts +++ b/client/src/app/modules/system/system.component.ts @@ -9,10 +9,12 @@ import { MatDialog, MatDialogModule } from '@angular/material/dialog'; import { CreateSystemComponent } from './create/create.component'; import { AgGridContainerComponent } from '../../shared/ag-grid/components/ag-grid-container/ag-grid-container.component'; import { AgSystemManagerComponent } from '../../shared/ag-grid/components/ag-system-manager/ag-system-manager.component'; +import { SystemArchiveComponent } from './components/system-archive/system-archive.component'; +import { MatIconModule } from '@angular/material/icon'; @Component({ selector: 'app-system', - imports: [AgGridAngular, MatButtonModule, MatDialogModule], + imports: [AgGridAngular, MatButtonModule, MatDialogModule, MatIconModule], providers: [DatePipe, { provide: LOCALE_ID, useValue: 'de-DE' }], templateUrl: './system.component.html', styleUrl: './system.component.scss' @@ -41,12 +43,15 @@ export class SystemComponent extends AgGridContainerComponent { // , onCellClicked: (event) => { this.deleteKey(event.data.id)} } ]; + } async loadSystems() { this.gridApi.setGridOption("loading", true); await this.api.refreshSystems(); this.gridApi.setGridOption("loading", false); + const a = await this.api.getSystemArchive(); + console.log(a) } onGridReady(params: GridReadyEvent) { @@ -81,4 +86,15 @@ export class SystemComponent extends AgGridContainerComponent { } }) } + + openArchive() { + this.dialog.open(SystemArchiveComponent, { + maxHeight: "calc(100vh - 24px)", + maxWidth: "calc(100vw - 24px)", + width: "50vw", + minWidth: "300px", + height: "70vh", + disableClose: true + }) + } } diff --git a/client/src/app/shared/ag-grid/components/ag-delete-system/ag-delete-system.component.html b/client/src/app/shared/ag-grid/components/ag-delete-system/ag-delete-system.component.html new file mode 100644 index 0000000..b8b66b1 --- /dev/null +++ b/client/src/app/shared/ag-grid/components/ag-delete-system/ag-delete-system.component.html @@ -0,0 +1,20 @@ +

Schließanlage {{key.name}} löschen?

+ +
+ warning +

+ {{key.name}} wirklich entfernen? +

+

+ + Gelöschte Schließanlagen können über das Archiv wiederhergestellt werden. +

+
+
+ + + + \ No newline at end of file diff --git a/client/src/app/shared/ag-grid/components/ag-delete-system/ag-delete-system.component.scss b/client/src/app/shared/ag-grid/components/ag-delete-system/ag-delete-system.component.scss new file mode 100644 index 0000000..e69de29 diff --git a/client/src/app/shared/ag-grid/components/ag-delete-system/ag-delete-system.component.spec.ts b/client/src/app/shared/ag-grid/components/ag-delete-system/ag-delete-system.component.spec.ts new file mode 100644 index 0000000..8172754 --- /dev/null +++ b/client/src/app/shared/ag-grid/components/ag-delete-system/ag-delete-system.component.spec.ts @@ -0,0 +1,23 @@ +import { ComponentFixture, TestBed } from '@angular/core/testing'; + +import { AgDeleteSystemComponent } from './ag-delete-system.component'; + +describe('AgDeleteSystemComponent', () => { + let component: AgDeleteSystemComponent; + let fixture: ComponentFixture; + + beforeEach(async () => { + await TestBed.configureTestingModule({ + imports: [AgDeleteSystemComponent] + }) + .compileComponents(); + + fixture = TestBed.createComponent(AgDeleteSystemComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); +}); diff --git a/client/src/app/shared/ag-grid/components/ag-delete-system/ag-delete-system.component.ts b/client/src/app/shared/ag-grid/components/ag-delete-system/ag-delete-system.component.ts new file mode 100644 index 0000000..fe17199 --- /dev/null +++ b/client/src/app/shared/ag-grid/components/ag-delete-system/ag-delete-system.component.ts @@ -0,0 +1,17 @@ +import { Component, inject } from '@angular/core'; +import { MatButtonModule } from '@angular/material/button'; +import { MatDialogModule, MatDialogRef, MAT_DIALOG_DATA } from '@angular/material/dialog'; +import { MatIconModule } from '@angular/material/icon'; +import { IKey } from '../../../../model/interface/key.interface'; + +@Component({ + selector: 'app-ag-delete-system', + imports: [MatDialogModule, MatButtonModule, MatIconModule], + templateUrl: './ag-delete-system.component.html', + styleUrl: './ag-delete-system.component.scss', +}) +export class AgDeleteSystemComponent { + readonly dialogRef = inject(MatDialogRef); + readonly key = inject(MAT_DIALOG_DATA); + +} diff --git a/client/src/app/shared/ag-grid/components/ag-system-manager/ag-system-manager.component.html b/client/src/app/shared/ag-grid/components/ag-system-manager/ag-system-manager.component.html index bc13d73..44e85c9 100644 --- a/client/src/app/shared/ag-grid/components/ag-system-manager/ag-system-manager.component.html +++ b/client/src/app/shared/ag-grid/components/ag-system-manager/ag-system-manager.component.html @@ -1 +1,2 @@ -
\ No newline at end of file +
+
\ No newline at end of file diff --git a/client/src/app/shared/ag-grid/components/ag-system-manager/ag-system-manager.component.ts b/client/src/app/shared/ag-grid/components/ag-system-manager/ag-system-manager.component.ts index 5ad052b..3b76452 100644 --- a/client/src/app/shared/ag-grid/components/ag-system-manager/ag-system-manager.component.ts +++ b/client/src/app/shared/ag-grid/components/ag-system-manager/ag-system-manager.component.ts @@ -6,6 +6,7 @@ import { ICellRendererAngularComp } from 'ag-grid-angular'; import { ICellRendererParams } from 'ag-grid-community'; import { ApiService } from '../../../api.service'; import { SystemManagerComponent } from '../../../../modules/system/components/system-manager/system-manager.component'; +import { AgDeleteSystemComponent } from '../ag-delete-system/ag-delete-system.component'; @Component({ selector: 'app-ag-system-manager', @@ -32,15 +33,39 @@ export class AgSystemManagerComponent implements ICellRendererAngularComp { openManager() { - const ref = this.dialog.open(SystemManagerComponent, { - data: this.system, - autoFocus: false, - maxHeight: "calc(100vh - 24px)", - maxWidth: "calc(100vw - 24px)", - width: "50vw", - minWidth: "300px", - height: "70vh", - disableClose: false - }) - } + const ref = this.dialog.open(SystemManagerComponent, { + data: this.system, + autoFocus: false, + maxHeight: "calc(100vh - 24px)", + maxWidth: "calc(100vw - 24px)", + width: "50vw", + minWidth: "300px", + height: "70vh", + disableClose: false + }) + } + + delete() { + const ref = this.dialog.open(AgDeleteSystemComponent, { + data: this.system, + autoFocus: false, + width: '500px', + maxWidth: 'calc(100vw - 24px)', + }) + + ref.afterClosed().subscribe({ + next: n => { + if (n) { + this.deleteThisKey(); + } + } + }) + } + + async deleteThisKey() { + this.params.api.setGridOption("loading", true); + await this.api.deleteSystem(this.system); + this.params.api.setGridOption("loading", false); + + } } diff --git a/client/src/app/shared/api.service.ts b/client/src/app/shared/api.service.ts index 1e06b33..8c08d76 100644 --- a/client/src/app/shared/api.service.ts +++ b/client/src/app/shared/api.service.ts @@ -6,6 +6,7 @@ import { IKey } from '../model/interface/key.interface'; import { ICylinder } from '../model/interface/cylinder.interface'; import { HotToastService } from '@ngxpert/hot-toast'; import { ICustomer } from '../model/interface/customer.interface'; +import { ISystem } from '../model/interface/keysystem.interface'; @Injectable({ providedIn: 'root' @@ -198,7 +199,7 @@ export class ApiService { */ deleteSystem(system: any): Promise { return new Promise(resolve => { - this.http.delete(`api/system${system.id}`).pipe( + this.http.delete(`api/system/${system.id}`).pipe( this.toast.observe({ loading: `Lösche Schließanlage ${system.name}...`, success: `Schließanlage ${system.name} wurde gelöscht.`, @@ -222,6 +223,21 @@ export class ApiService { return this.http.put(`api/key/${id}/restore`, null); } + restoreSystem(system: ISystem): Promise { + return new Promise(resolve => { + this.http.put(`api/system/${system.id}/restore`, null).subscribe({ + next: () => { + this.refreshSystems(); + resolve(true); + }, + error: () => { + this.toast.error('Schließanlage konnte nicht wiederhergestellt werden'); + resolve(false) + } + }) + }) + } + private getCylinders(): Observable { return this.http.get('api/cylinder'); } @@ -240,6 +256,20 @@ export class ApiService { }) } + getSystemArchive(): Promise { + return new Promise(resolve => { + this.http.get('api/system/archive').subscribe({ + next: val => { + return resolve(val); + }, + error: () => { + this.toast.error('Gelöschte Schließanlagen konnten nicht geladen werden'); + return resolve([]) + } + }) + }) + } + /** * Aktualisiert die Zylinder im Behaviour Subject * cylinders