Archive und Logging

This commit is contained in:
Bastian Wagner
2026-02-19 16:19:46 +01:00
parent ef45e91141
commit 7bd6dfae27
28 changed files with 358 additions and 44 deletions

View File

@@ -7,6 +7,7 @@ import { provideHttpClient, withInterceptors } from '@angular/common/http';
import { tokenInterceptor } from './core/interceptor/token.interceptor';
import { provideAnimationsAsync } from '@angular/platform-browser/animations/async';
import { provideServiceWorker } from '@angular/service-worker';
import { OVERLAY_DEFAULT_CONFIG } from "@angular/cdk/overlay";
export const appConfig: ApplicationConfig = {
providers: [provideZoneChangeDetection({ eventCoalescing: true }), provideRouter(routes), provideHttpClient(withInterceptors([tokenInterceptor]))
@@ -17,11 +18,18 @@ export const appConfig: ApplicationConfig = {
theme: 'toast',
autoClose: true,
dismissible: false,
duration: 5000
duration: 5000,
}),
provideAnimationsAsync(), provideServiceWorker('ngsw-worker.js', {
enabled: !isDevMode(),
registrationStrategy: 'registerWhenStable:30000'
})
}),
{
provide: OVERLAY_DEFAULT_CONFIG,
useValue: {
usePopover: false,
},
},
]
};

View File

@@ -0,0 +1,15 @@
<h2 mat-dialog-title>Gelöschte Zylinder</h2>
<mat-dialog-content>
@if(myTheme && gridOptions) {
<ag-grid-angular
style="width: 100%; height: 100%;"
(gridReady)="onGridReady($event)"
[gridOptions]="gridOptions!"
[theme]="myTheme"
/>
}
</mat-dialog-content>
<mat-dialog-actions>
<button matButton mat-dialog-close>Schließen</button>
</mat-dialog-actions>

View File

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

View File

@@ -0,0 +1,78 @@
import { Component, inject, LOCALE_ID } from '@angular/core';
import { AgGridContainerComponent } from '../../../../shared/ag-grid/components/ag-grid-container/ag-grid-container.component';
import { CommonModule, DatePipe } from '@angular/common';
import { HotToastService } from '@ngxpert/hot-toast';
import { ApiService } from '../../../../shared/api.service';
import { GridApi, GridOptions, GridReadyEvent } from 'ag-grid-community';
import { HELPER } from '../../../../shared/helper.service';
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 { ICylinder } from '../../../../model/interface/cylinder.interface';
@Component({
selector: 'app-cylinder-archive',
imports: [MatDialogModule, AgGridAngular, MatButtonModule, MatIconModule, CommonModule],
providers: [DatePipe, { provide: LOCALE_ID, useValue: 'de-DE' }],
templateUrl: './cylinder-archive.component.html',
styleUrl: './cylinder-archive.component.scss',
})
export class CylinderArchiveComponent extends AgGridContainerComponent {
private api: ApiService = inject(ApiService);
private datePipe = inject(DatePipe);
private toast = inject(HotToastService);
gridApi!: GridApi;
gridOptions: GridOptions = HELPER.getGridOptions();
constructor() {
super();
this.createGridOptions();
}
private createGridOptions() {
this.gridOptions.columnDefs = [
{ colId: 'name', field: 'name' , headerName: 'Name', flex: 1, editable: true, sort: 'asc', filter: true },
{ colId: 'nr', field: 'nr' , headerName: 'Name', flex: 1, editable: true, filter: true },
{
field: 'deletedAt'
, headerName: 'Gelöscht'
, width: 160
, cellRenderer: (data: any) => this.datePipe.transform(new Date(data.value), 'short')
},
{
width: 40,
cellRenderer: () => '<div class="icon-btn-sm restore icon-btn-xs" ></div>',
onCellClicked: (event) => { this.restoreCylinder(event.data);},
tooltipValueGetter: () => 'Wiederherstellen',
sortable: false
}
];
this.gridOptions.rowHeight = 36;
this.gridOptions.overlayNoRowsTemplate = 'Bisher wurden keine Zylinder gelöscht. Sobald dies der Fall ist, werden sie hier angezeigt.';
}
async restoreCylinder(data: ICylinder) {
this.gridApi.setGridOption("loading", true);
await this.api.restoreCylinder(data.id);
this.loadCylinders();
}
onGridReady(params: GridReadyEvent) {
this.gridApi = params.api;
this.loadCylinders();
}
async loadCylinders() {
this.gridApi.setGridOption("loading", true);
const cylinders = await this.api.getCylinderArchive();
this.gridApi.setGridOption("rowData", cylinders);
this.gridApi.setGridOption("loading", false);
}
}

View File

@@ -8,5 +8,5 @@
}
<div class="floating-btn-container">
<button mat-flat-button class="btn-create mat-elevation-z8" (click)="openCreateCylinder()" >Zylinder anlegen</button>
<button mat-mini-fab disabled><mat-icon>inventory_2</mat-icon></button>
<button mat-mini-fab (click)="openArchive()"><mat-icon>inventory_2</mat-icon></button>
</div>

View File

@@ -10,6 +10,7 @@ import { CreateCylinderComponent } from './components/create-cylinder/create-cyl
import { MatIconModule } from '@angular/material/icon';
import { MatButtonModule } from '@angular/material/button';
import { AgGridContainerComponent } from '../../shared/ag-grid/components/ag-grid-container/ag-grid-container.component';
import { CylinderArchiveComponent } from './components/cylinder-archive/cylinder-archive.component';
@Component({
selector: 'app-cylinder',
@@ -76,4 +77,14 @@ export class CylinderComponent extends AgGridContainerComponent {
}
});
}
openArchive() {
this.dialog.open(CylinderArchiveComponent, {
maxHeight: "calc(100vh - 24px)",
maxWidth: "calc(100vw - 24px)",
width: "50vw",
minWidth: "min(700px,calc(100vw - 24px))",
height: "70vh",
})
}
}

View File

@@ -1,10 +1,13 @@
<h2 mat-dialog-title>Gelöschte Schlüssel</h2>
<mat-dialog-content>
<ag-grid-angular
@if(myTheme) {
<ag-grid-angular
style="width: 100%; height: 100%;"
(gridReady)="onGridReady($event)"
[gridOptions]="gridOptions!"
[theme]="myTheme"
/>
}
</mat-dialog-content>
<mat-dialog-actions>

View File

@@ -11,6 +11,7 @@ import { IKey } from '../../../../model/interface/key.interface';
import { HotToastService } from '@ngxpert/hot-toast';
import { AgLoadingComponent } from '../../../../shared/ag-grid/components/ag-loading/ag-loading.component';
import { HELPER } from '../../../../shared/helper.service';
import { AgGridContainerComponent } from '../../../../shared/ag-grid/components/ag-grid-container/ag-grid-container.component';
@Component({
selector: 'app-archive',
@@ -19,7 +20,7 @@ import { HELPER } from '../../../../shared/helper.service';
templateUrl: './archive.component.html',
styleUrl: './archive.component.scss'
})
export class ArchiveComponent {
export class ArchiveComponent extends AgGridContainerComponent {
private api: ApiService = inject(ApiService);
private datePipe = inject(DatePipe);
private toast = inject(HotToastService);
@@ -31,6 +32,7 @@ export class ArchiveComponent {
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 },
@@ -87,6 +89,9 @@ export class ArchiveComponent {
},
error: () => {
this.gridApi.setGridOption("loading", false);
},
complete: () => {
this.api.refreshKeys();
}
});
}

View File

@@ -1,10 +1,13 @@
<h2 mat-dialog-title>Verlorene Schlüssel</h2>
<mat-dialog-content>
<ag-grid-angular
@if(myTheme) {
<ag-grid-angular
style="width: 100%; height: 100%;"
(gridReady)="onGridReady($event)"
[gridOptions]="gridOptions!"
[theme]="myTheme"
/>
}
</mat-dialog-content>
<mat-dialog-actions>
<button matButton [mat-dialog-close]="dataChanged">Schließen</button>

View File

@@ -10,6 +10,7 @@ import { HELPER } from '../../../../shared/helper.service';
import { AgGridAngular } from 'ag-grid-angular';
import { LostKeyComponent } from '../lost-key/lost-key.component';
import { MatButtonModule } from '@angular/material/button';
import { AgGridContainerComponent } from '../../../../shared/ag-grid/components/ag-grid-container/ag-grid-container.component';
@Component({
selector: 'app-lost-keys',
@@ -18,7 +19,7 @@ import { MatButtonModule } from '@angular/material/button';
templateUrl: './lost-keys.component.html',
styleUrl: './lost-keys.component.scss'
})
export class LostKeysComponent {
export class LostKeysComponent extends AgGridContainerComponent {
private api: ApiService = inject(ApiService);
private datePipe = inject(DatePipe);
private dialog: MatDialog = inject(MatDialog);
@@ -31,6 +32,7 @@ export class LostKeysComponent {
gridOptions: GridOptions = HELPER.getGridOptions();
constructor() {
super();
this.gridOptions.columnDefs = [
{ colId: 'name', field: 'name', headerName: 'Name', sort: 'asc', flex: 1, filter: true },
{ colId: 'nr', field: 'nr', headerName: 'Schlüsselnummer', flex: 1, filter: true },
@@ -83,6 +85,7 @@ export class LostKeysComponent {
next: () => {
this.toast.success('Schlüssel als gefunden markiert');
this.loadLostKeys();
this.api.refreshKeys();
}
});
this.dataChanged = true;

View File

@@ -4,18 +4,21 @@
<mat-dialog-content>
<div style="display: flex; flex-direction: column; height: calc(100% - 4px);" class="gap-2">
<div class="flex-auto">
<ag-grid-angular
@if(myTheme) {
<ag-grid-angular
style="width: 100%; height: 100%;"
(gridReady)="onGridReady($event)"
[gridOptions]="gridOptions!"
[theme]="myTheme"
/>
}
</div>
<div class="flex gap-2 items-center p-4 bg-gray-50 rounded-md shadow-sm">
<mat-form-field class="flex-1">
<mat-label>Email</mat-label>
<input matInput [(ngModel)]="email" placeholder="beispiel@email.com">
<mat-hint>Emailadresse des neuen Users eingeben</mat-hint>
<mat-hint>Emailadresse des Benutzers eingeben</mat-hint>
<mat-icon matPrefix class="text-gray-400 mr-2">email</mat-icon>
</mat-form-field>
<button matButton="elevated"

View File

@@ -15,6 +15,7 @@ import { HttpErrorResponse } from '@angular/common/http';
import { RemoveManagerPopupComponent } from '../remove-manager-popup/remove-manager-popup.component';
import { IUser } from '../../../../model/interface/user.interface';
import { MatIconModule } from '@angular/material/icon';
import { AgGridContainerComponent } from '../../../../shared/ag-grid/components/ag-grid-container/ag-grid-container.component';
@Component({
selector: 'app-system-manager',
@@ -22,7 +23,7 @@ import { MatIconModule } from '@angular/material/icon';
templateUrl: './system-manager.component.html',
styleUrl: './system-manager.component.scss'
})
export class SystemManagerComponent {
export class SystemManagerComponent extends AgGridContainerComponent {
gridApi!: GridApi;
@@ -39,10 +40,11 @@ export class SystemManagerComponent {
constructor() {
super();
this.gridOptions.columnDefs = [
{ colId: 'name', field: 'firstName', headerName: 'Name', sort: 'asc', flex: 1, cellRenderer: (data: any) => data.data.firstName + ' ' + data.data.lastName, sortable: true, filter: true},
{ colId: 'mail', field: 'username', headerName: 'E-Mail', flex: 1},
{ colId: 'delete', headerName: '', width: 50,
{ colId: 'delete', headerName: '', width: 50, sortable: false,
cellRenderer: (params: any) => {
if (this.authService.user.username == params.data.username) return '';
return `<div class="delete icon icon-btn-xs"></div>`;

View File

@@ -1,20 +1,19 @@
import { DatePipe } from '@angular/common';
import { Component, inject } from '@angular/core';
import { Component, inject, LOCALE_ID } from '@angular/core';
import { AgGridAngular } from 'ag-grid-angular';
import { GridApi, GridOptions, GridReadyEvent, Theme, ThemeDefaultParams } from 'ag-grid-community';
import { GridApi, GridOptions, GridReadyEvent } from 'ag-grid-community';
import { ApiService } from '../../shared/api.service';
import { HELPER } from '../../shared/helper.service';
import { MatButtonModule } from '@angular/material/button';
import { MatDialog, MatDialogModule } from '@angular/material/dialog';
import { CreateSystemComponent } from './create/create.component';
import { AgSystemManagerComponent } from '../../shared/ag-grid/components/ag-system-manager/ag-system-manager.component';
import { AgGridService } from '../../shared/ag-grid/ag-grid.service';
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';
@Component({
selector: 'app-system',
imports: [AgGridAngular, MatButtonModule, MatDialogModule],
providers: [DatePipe],
providers: [DatePipe, { provide: LOCALE_ID, useValue: 'de-DE' }],
templateUrl: './system.component.html',
styleUrl: './system.component.scss'
})
@@ -31,6 +30,7 @@ export class SystemComponent extends AgGridContainerComponent {
super();
this.gridOptions.columnDefs = [
{ colId: 'name', field: 'name', headerName: 'Name', sort: 'asc', flex: 1},
{ colId: 'cylinderCount', field: 'cylinders', headerName: 'Zylinderanzahl', flex: 0, cellRenderer: (data: any) => data.value.length},
{ 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)) : '-' },
{

View File

@@ -27,20 +27,11 @@ export class AgDeleteCylinderComponent extends AgBaseComponentComponent {
})
}
deleteThisCylinder() {
this.api.deleteCylinder(this.params.data)
.pipe(
this.toast.observe({
loading: 'Löschen...',
success: 'Gelöscht!',
error: 'Konnte nicht gelöscht werden'
})
).subscribe({
next: () => {
const rows = this.params.api.getGridOption("rowData")?.filter(r => r.id != this.params.data.id);
this.params.api.setGridOption("rowData", rows);
}
})
async deleteThisCylinder() {
await this.api.deleteCylinder(this.params.data);
const rows = this.params.api.getGridOption("rowData")?.filter(r => r.id != this.params.data.id);
this.params.api.setGridOption("rowData", rows);
}
}

View File

@@ -1,6 +1,6 @@
import { HttpClient } from '@angular/common/http';
import { inject, Injectable } from '@angular/core';
import { BehaviorSubject, Observable } from 'rxjs';
import { BehaviorSubject, map, Observable } from 'rxjs';
import { IUser } from '../model/interface/user.interface';
import { IKey } from '../model/interface/key.interface';
import { ICylinder } from '../model/interface/cylinder.interface';
@@ -121,6 +121,20 @@ export class ApiService {
return this.http.get<ICylinder[]>('api/cylinder');
}
getCylinderArchive(): Promise<ICylinder[]> {
return new Promise<ICylinder[]>(resolve => {
this.http.get<ICylinder[]>('api/cylinder/archive').subscribe({
next: val => {
return resolve(val);
},
error: () => {
this.toast.error('Gelöschte Zylinder konnten nicht geladen werden');
return resolve([])
}
})
})
}
/**
* Aktualisiert die Zylinder im Behaviour Subject
* cylinders
@@ -140,8 +154,37 @@ export class ApiService {
})
}
deleteCylinder(cylinder: ICylinder): Observable<any> {
return this.http.delete(`api/cylinder/${cylinder.id}`)
deleteCylinder(cylinder: ICylinder): Promise<boolean> {
return new Promise<boolean>(resolve => {
this.http.delete(`api/cylinder/${cylinder.id}`).pipe(
this.toast.observe({
loading: `Lösche Zylinder ${cylinder.name}...`,
success: 'Zylinder gelöscht',
error: 'Es ist ein Fehler aufgetreten'
})
).subscribe({
next: () => resolve(true),
error: () => resolve(false),
complete: () => this.refreshCylinders()
})
})
}
restoreCylinder(id: string): Promise<boolean> {
return new Promise<boolean>(resolve => {
this.http.put(`api/cylinder/${id}/restore`, null).pipe(
this.toast.observe({
loading: 'Stelle wiederher...',
success: 'Zylinder wiederhergestellt',
error: 'Es ist ein Fehler aufgetreten'
})).subscribe({
next: () => resolve(true),
error: () => resolve(false),
complete: () => this.refreshCylinders()
})
})
}
deleteUser(id: string) {