Logging und sowas

This commit is contained in:
Bastian Wagner
2026-02-20 10:28:48 +01:00
parent 29bfffc505
commit 4e051a1f40
14 changed files with 87 additions and 22 deletions

View File

@@ -58,7 +58,7 @@ export class User implements IUser {
@DeleteDateColumn() @DeleteDateColumn()
deletedAt: Date; deletedAt: Date;
@OneToOne(() => UserSettings, (settings) => settings.user, { cascade: true, onDelete: 'CASCADE', onUpdate: 'NO ACTION', }) @OneToOne(() => UserSettings, (settings) => settings.user, { cascade: true, onUpdate: 'NO ACTION', })
settings: UserSettings; settings: UserSettings;
accessToken?: string; accessToken?: string;

View File

@@ -6,7 +6,7 @@ export class UserSettings {
@PrimaryGeneratedColumn('uuid') @PrimaryGeneratedColumn('uuid')
id: string; id: string;
@OneToOne(() => User, (user) => user.settings) @OneToOne(() => User, (user) => user.settings, { onDelete: 'CASCADE' })
@JoinColumn() @JoinColumn()
user: User; user: User;

View File

@@ -1,4 +1,6 @@
import { PartialType } from '@nestjs/mapped-types'; import { PartialType } from '@nestjs/mapped-types';
import { CreateSystemDto } from './create-system.dto'; import { CreateSystemDto } from './create-system.dto';
export class UpdateSystemDto extends PartialType(CreateSystemDto) {} export class UpdateSystemDto extends PartialType(CreateSystemDto) {
id: string;
}

View File

@@ -48,9 +48,9 @@ export class SystemController {
return this.systemService.findOne(id); return this.systemService.findOne(id);
} }
@Put(':id') @Put()
update(@Param('id') id: string, @Body() updateSystemDto: UpdateSystemDto) { update(@Req() req: AuthenticatedRequest, @Body() updateSystemDto: UpdateSystemDto) {
return this.systemService.update(id, updateSystemDto); return this.systemService.update(req.user, updateSystemDto);
} }
@Delete(':id') @Delete(':id')

View File

@@ -5,10 +5,11 @@ import { AuthModule } from '../auth/auth.module';
import { DatabaseModule } from 'src/shared/database/database.module'; import { DatabaseModule } from 'src/shared/database/database.module';
import { MailModule } from '../mail/mail.module'; import { MailModule } from '../mail/mail.module';
import { ConfigService } from '@nestjs/config'; import { ConfigService } from '@nestjs/config';
import { SharedServiceModule } from 'src/shared/service/shared.service.module';
@Module({ @Module({
controllers: [SystemController], controllers: [SystemController],
providers: [SystemService, ConfigService], providers: [SystemService, ConfigService],
imports: [AuthModule, DatabaseModule, MailModule], imports: [AuthModule, DatabaseModule, MailModule, SharedServiceModule],
}) })
export class SystemModule {} export class SystemModule {}

View File

@@ -6,6 +6,7 @@ import { User } from 'src/model/entitites';
import { IUser } from 'src/model/interface'; import { IUser } from 'src/model/interface';
import { MailService } from '../mail/mail.service'; import { MailService } from '../mail/mail.service';
import { ConfigService } from '@nestjs/config'; import { ConfigService } from '@nestjs/config';
import { ActivityHelperService } from 'src/shared/service/activity.logger.service';
@Injectable() @Injectable()
export class SystemService { export class SystemService {
@@ -14,7 +15,8 @@ export class SystemService {
private userRepo: UserRepository, private userRepo: UserRepository,
private systemActivityRepo: ActivityRepository, private systemActivityRepo: ActivityRepository,
private mailService: MailService, private mailService: MailService,
private readonly configService: ConfigService private readonly configService: ConfigService,
private readonly activityService: ActivityHelperService
) {} ) {}
get isDevelopMode(): boolean { get isDevelopMode(): boolean {
@@ -59,12 +61,21 @@ export class SystemService {
} }
// eslint-disable-next-line @typescript-eslint/no-unused-vars // eslint-disable-next-line @typescript-eslint/no-unused-vars
update(id: string, updateSystemDto: UpdateSystemDto) { async update(user: User, updateSystemDto: UpdateSystemDto) {
throw new HttpException( if (!user || !user.id || !updateSystemDto.id) { throw new HttpException('forbidden', HttpStatus.FORBIDDEN); }
`This action updates a #${id} system but is not implemented`, console.log(updateSystemDto);
HttpStatus.NOT_IMPLEMENTED, const system = await this.systemRepo.findOne({ where: { id: updateSystemDto.id, managers: { id: user.id } }, withDeleted: true });
);
return `This action updates a #${id} system`; if (!system) { throw new HttpException('forbidden', HttpStatus.FORBIDDEN); }
if (system.name !== updateSystemDto.name) {
await this.activityService.logSystemRenamed({ system, newName: updateSystemDto.name, user })
system.name = updateSystemDto.name;
}
return true;
} }
async remove(id: string) { async remove(id: string) {

View File

@@ -13,8 +13,8 @@ export class UserService {
private readonly systemActivityRepo: ActivityRepository, private readonly systemActivityRepo: ActivityRepository,
private readonly userSettingsRepository: UserSettingsRepository, private readonly userSettingsRepository: UserSettingsRepository,
private readonly helper: HelperService, private readonly helper: HelperService,
) { ) {}
}
getAllUsers(): Promise<User[]> { getAllUsers(): Promise<User[]> {

View File

@@ -14,6 +14,17 @@ export class ActivityHelperService {
private readonly cylinderRepo: CylinderRepository, private readonly cylinderRepo: CylinderRepository,
) {} ) {}
async logSystemRenamed({system, newName, user}: { system: KeySystem, newName: string, user: User}) {
let msg = `Schließanlage von ${system.name} zu ${newName} umbenannt`;
return this.activityRepo.save(
this.activityRepo.create({
system,
user,
message: msg,
}))
}
async logDeleteKey(user: User, key: Key, system?: KeySystem) { async logDeleteKey(user: User, key: Key, system?: KeySystem) {
if (!key || !user) { return; } if (!key || !user) { return; }

View File

@@ -18,6 +18,7 @@
<mat-hint>Zylinderlänge und co.</mat-hint> <mat-hint>Zylinderlänge und co.</mat-hint>
</mat-form-field> </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">
@@ -26,6 +27,7 @@
} }
</mat-select> </mat-select>
<mat-hint>Zu welcher Schließanlage gehört der Zylinder?</mat-hint> <mat-hint>Zu welcher Schließanlage gehört der Zylinder?</mat-hint>
<mat-progress-bar mode="indeterminate" *ngIf="isLoading"></mat-progress-bar>
</mat-form-field> </mat-form-field>
</form> </form>

View File

@@ -9,10 +9,12 @@ import { MatInputModule } from '@angular/material/input';
import { MatSelectModule } from '@angular/material/select'; import { MatSelectModule } from '@angular/material/select';
import { MatButtonModule } from '@angular/material/button'; import { MatButtonModule } from '@angular/material/button';
import { MatIconModule } from '@angular/material/icon'; import { MatIconModule } from '@angular/material/icon';
import { MatProgressBarModule } from '@angular/material/progress-bar';
import { CommonModule } from '@angular/common';
@Component({ @Component({
selector: 'app-create-cylinder', selector: 'app-create-cylinder',
imports: [MatFormFieldModule, MatInputModule, MatDialogModule, ReactiveFormsModule, FormsModule, MatSelectModule, MatButtonModule, MatIconModule], imports: [MatFormFieldModule, MatInputModule, MatDialogModule, ReactiveFormsModule, FormsModule, MatSelectModule, MatButtonModule, MatIconModule, MatProgressBarModule, CommonModule],
templateUrl: './create-cylinder.component.html', templateUrl: './create-cylinder.component.html',
styleUrl: './create-cylinder.component.scss' styleUrl: './create-cylinder.component.scss'
}) })
@@ -22,6 +24,7 @@ export class CreateCylinderComponent {
readonly dialogRef = inject(MatDialogRef<CreateCylinderComponent>); readonly dialogRef = inject(MatDialogRef<CreateCylinderComponent>);
systems: any[] = []; systems: any[] = [];
isLoading = true;
createForm = new FormGroup({ createForm = new FormGroup({
name: new FormControl<string | null>(null, Validators.required), name: new FormControl<string | null>(null, Validators.required),
@@ -35,6 +38,13 @@ export class CreateCylinderComponent {
this.systems = systems; this.systems = systems;
} }
}); });
this.loadCylinders();
}
private async loadCylinders() {
this.isLoading = true;
await this.api.refreshSystems();
this.isLoading = false;
} }
save() { save() {

View File

@@ -29,12 +29,12 @@
<mat-label>Schließzylinder</mat-label> <mat-label>Schließzylinder</mat-label>
<mat-select formControlName="cylinder" multiple> <mat-select formControlName="cylinder" multiple>
@for (item of cylinders; track $index) { @for (item of cylinders; track $index) {
<mat-option [value]="item">{{ item.name }}</mat-option> <mat-option [value]="item">{{ item.name }} ({{item.system.name}}) </mat-option>
} }
</mat-select> </mat-select>
<mat-hint>Wo sperrt der Schlüssel?</mat-hint> <mat-hint>Wo sperrt der Schlüssel?</mat-hint>
</mat-form-field> </mat-form-field>
<button mat-icon-button (click)="openSelectMultipleCylinders()" style="margin-bottom: 12px;"> <button mat-icon-button (click)="openSelectMultipleCylinders()" style="margin-bottom: 12px;" matTooltip="Zylinderauswahl in neuem Fenster öffnen">
<mat-icon>open_in_new</mat-icon> <mat-icon>open_in_new</mat-icon>
</button> </button>
</div> </div>

View File

@@ -14,10 +14,11 @@ import { MatIconModule } from '@angular/material/icon';
import {MatCheckboxModule} from '@angular/material/checkbox'; import {MatCheckboxModule} from '@angular/material/checkbox';
import { IKey } from '../../../model/interface/key.interface'; import { IKey } from '../../../model/interface/key.interface';
import { ICylinder } from '../../../model/interface/cylinder.interface'; import { ICylinder } from '../../../model/interface/cylinder.interface';
import { MatTooltipModule } from '@angular/material/tooltip';
@Component({ @Component({
selector: 'app-create', selector: 'app-create',
imports: [MatDialogModule, MatButtonModule, ReactiveFormsModule, FormsModule, MatFormFieldModule, MatInputModule, MatSelectModule, MatDialogModule, MatIconModule, MatCheckboxModule], imports: [MatDialogModule, MatButtonModule, ReactiveFormsModule, FormsModule, MatFormFieldModule, MatInputModule, MatSelectModule, MatDialogModule, MatIconModule, MatCheckboxModule, MatTooltipModule],
templateUrl: './create.component.html', templateUrl: './create.component.html',
styleUrl: './create.component.scss' styleUrl: './create.component.scss'
}) })

View File

@@ -1,7 +1,7 @@
import { DatePipe } from '@angular/common'; import { DatePipe } from '@angular/common';
import { Component, inject, LOCALE_ID } from '@angular/core'; import { Component, inject, LOCALE_ID } from '@angular/core';
import { AgGridAngular } from 'ag-grid-angular'; import { AgGridAngular } from 'ag-grid-angular';
import { GridApi, GridOptions, GridReadyEvent } from 'ag-grid-community'; import { CellEditingStoppedEvent, GridApi, GridOptions, GridReadyEvent } from 'ag-grid-community';
import { ApiService } from '../../shared/api.service'; import { ApiService } from '../../shared/api.service';
import { HELPER } from '../../shared/helper.service'; import { HELPER } from '../../shared/helper.service';
import { MatButtonModule } from '@angular/material/button'; import { MatButtonModule } from '@angular/material/button';
@@ -29,7 +29,7 @@ export class SystemComponent extends AgGridContainerComponent {
constructor() { constructor() {
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, editable: true},
{ colId: 'cylinderCount', field: 'cylinders', headerName: 'Zylinderanzahl', flex: 0, cellRenderer: (data: any) => data.value?.length || 0}, { 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)) : '-' },
@@ -51,6 +51,7 @@ export class SystemComponent 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.api.systems.asObservable().subscribe({ this.api.systems.asObservable().subscribe({
next: systems => { next: systems => {
this.gridApi.setGridOption("rowData", systems); this.gridApi.setGridOption("rowData", systems);
@@ -60,6 +61,14 @@ export class SystemComponent extends AgGridContainerComponent {
this.loadSystems(); this.loadSystems();
} }
async cellEditEnd(event: CellEditingStoppedEvent) {
const key: any = event.data;
if (!event.valueChanged || event.newValue == event.oldValue) { return; }
this.api.updateSystem(key)
}
openCreateSystem() { openCreateSystem() {
this.dialog.open(CreateSystemComponent).afterClosed().subscribe({ this.dialog.open(CreateSystemComponent).afterClosed().subscribe({
next: sys => { next: sys => {

View File

@@ -92,6 +92,24 @@ export class ApiService {
}) })
} }
updateSystem(cylinder: any): Promise<boolean> {
return new Promise<boolean>(resolve => {
this.http.put('api/system', cylinder).pipe(
this.toast.observe({
loading: `Schließanlage ${cylinder} wird gespeichert...`,
success: `Schließanlage ${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);
} }