This commit is contained in:
Bastian Wagner
2024-12-27 20:57:37 +01:00
parent 5194298fa6
commit 5363d0880f
33 changed files with 640 additions and 50 deletions

View File

@@ -16,7 +16,9 @@
<button mat-button routerLink="/keys" routerLinkActive="mat-elevation-z1">Schlüssel</button>
<button mat-button routerLink="/cylinders" routerLinkActive="mat-elevation-z1">Zylinder</button>
<button mat-button routerLink="/systems" routerLinkActive="mat-elevation-z1">System</button>
<button mat-button routerLink="/users" routerLinkActive="mat-elevation-z1">Alle User</button>
@if (isAdmin) {
<button mat-button routerLink="/users" routerLinkActive="mat-elevation-z1">Alle User</button>
}
</mat-drawer>

View File

@@ -23,4 +23,8 @@ export class LayoutComponent {
get userName(): string {
return `${this.authService.user.firstName} ${this.authService.user.lastName}`
}
get isAdmin(): boolean {
return this.authService.user.role === 'admin';
}
}

View File

@@ -158,7 +158,8 @@ export class HandoverDialogComponent {
this._bottomSheet.open(BottomSheetCreateCustomer).afterDismissed().subscribe({
next: async n => {
if (!n) { return; }
await this.createCustomer(val.customer);
const customer = await this.createCustomer(val.customer);
dto.customer = customer;
this.saveIt(dto);
}
})
@@ -168,7 +169,7 @@ export class HandoverDialogComponent {
}
saveIt(data: any) {
this.api.handoverKey(data)
this.api.handoverKey(data)
.pipe(
this.toast.observe({
loading: 'Speichern...',
@@ -178,17 +179,20 @@ export class HandoverDialogComponent {
)
.subscribe({
next: n => {
this.dialogRef.close(data.direction == 'out')
this.dialogRef.close(data.direction == 'out');
}
})
}
createCustomer(name: string) {
createCustomer(name: string): Promise<any> {
this.isLoading = true;
this.api.createCustomer({ name, system: this.data.cylinder.system}).subscribe({
next: n => {
this.isLoading = false;
}
return new Promise((resolve, reject) => {
this.api.createCustomer({ name, system: this.data.cylinder.system}).subscribe({
next: n => {
this.isLoading = false;
resolve(n);
}
})
})
}

View File

@@ -14,7 +14,7 @@
<mat-form-field>
<mat-label>Schlüsselnummer</mat-label>
<input type="number" matInput formControlName="nr" min="0" max="999999999999">
<input type="text" matInput formControlName="nr" maxlength="100">
<mat-hint>Nummer auf dem Schlüssel</mat-hint>
</mat-form-field>

View File

@@ -1,7 +0,0 @@
.floating-btn-container {
position: absolute;
bottom: 48px;
right: 24px;
display: flex;
gap: 12px;
}

View File

@@ -52,6 +52,7 @@ export class KeysComponent {
cellEditorPopup: false
},
{ colId: 'system', field: 'cylinder.system' , headerName: 'Schließanlage', flex: 1, editable: false, filter: true, cellRenderer: (data: any) => {return data.value?.name} },
{ colId: 'customer', field: 'customer' , headerName: 'Kunde', flex: 1, editable: false, filter: true, cellRenderer: (data: any) => {return data.value?.name} },
{
field: 'createdAt'
, headerName: 'Erstellt'

View File

@@ -0,0 +1,14 @@
<h2 mat-dialog-title>Manager</h2>
<mat-dialog-content>
<ag-grid-angular
style="width: 100%; height: 100%;"
(gridReady)="onGridReady($event)"
[gridOptions]="gridOptions!"
/>
</mat-dialog-content>
<mat-dialog-actions>
<button mat-button [mat-dialog-close]="true">Schließen</button>
</mat-dialog-actions>

View File

@@ -0,0 +1,3 @@
:host {
min-height: 500px;
}

View File

@@ -0,0 +1,64 @@
import { ComponentFixture, TestBed } from '@angular/core/testing';
import { SystemManagerComponent } from './system-manager.component';
import { MAT_DIALOG_DATA, MatDialogModule, MatDialogRef } from '@angular/material/dialog';
import { AgGridAngular } from 'ag-grid-angular';
import { ApiService } from '../../../../shared/api.service';
import { HotToastService } from '@ngxpert/hot-toast';
import { MockApiService } from '../../../../../../mocks/services/mock.api.service';
import { GridReadyEvent } from 'ag-grid-community';
describe('SystemManagerComponent', () => {
let component: SystemManagerComponent;
let fixture: ComponentFixture<SystemManagerComponent>;
let api: ApiService;
beforeEach(async () => {
await TestBed.configureTestingModule({
imports: [SystemManagerComponent, AgGridAngular, MatDialogModule],
providers: [
HotToastService,
{ provide: ApiService, useClass: MockApiService },
{
provide: MatDialogRef,
useValue: []
},
{
provide: MAT_DIALOG_DATA,
useValue: []
}
]
})
.compileComponents();
fixture = TestBed.createComponent(SystemManagerComponent);
component = fixture.componentInstance;
api = component['api']
fixture.detectChanges();
});
it('should create', () => {
expect(component).toBeTruthy();
});
it('should initialize gridApi and gridColumnApi on gridReady and fill data', () => {
// Mock des GridReadyEvent
let mockData = [{ id: 1, name: 'Test' }];
const mockGridReadyEvent: GridReadyEvent = {
api: { setGridOption: jest.fn() },
columnApi: { someColumnApiMethod: jest.fn() },
type: 'gridReady',
} as any;
// Methode aufrufen
component.onGridReady(mockGridReadyEvent);
// Assertions
expect(component.gridApi).toBe(mockGridReadyEvent.api);
expect(api.getSystemManagers).toHaveBeenCalled();
expect(component.gridApi.setGridOption).toHaveBeenCalled();
});
});

View File

@@ -0,0 +1,50 @@
import { Component, inject } from '@angular/core';
import { GridApi, GridOptions, GridReadyEvent } from 'ag-grid-community';
import { HELPER } from '../../../../shared/helper.service';
import { AgGridAngular } from 'ag-grid-angular';
import { MatDialogRef, MAT_DIALOG_DATA, MatDialog, MatDialogModule } from '@angular/material/dialog';
import { HotToastService } from '@ngxpert/hot-toast';
import { ApiService } from '../../../../shared/api.service';
@Component({
selector: 'app-system-manager',
standalone: true,
imports: [AgGridAngular, MatDialogModule],
templateUrl: './system-manager.component.html',
styleUrl: './system-manager.component.scss'
})
export class SystemManagerComponent {
gridApi!: GridApi;
gridOptions: GridOptions = HELPER.getGridOptions();
readonly dialogRef = inject(MatDialogRef<SystemManagerComponent>);
readonly system = inject<any>(MAT_DIALOG_DATA);
private api: ApiService = inject(ApiService);
private dialog: MatDialog = inject(MatDialog);
private toast = inject(HotToastService);
constructor() {
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},
]
}
onGridReady(params: GridReadyEvent) {
this.gridApi = params.api;
this.loadManagers();
}
loadManagers() {
this.api.getSystemManagers(this.system.id).subscribe({
next: n => {
this.gridApi.setGridOption("rowData", n);
this.gridApi.setGridOption("loading", false);
}
})
}
}

View File

@@ -0,0 +1,19 @@
<h2 mat-dialog-title>Neuen Schlüssel anlegen</h2>
<mat-dialog-content>
<form [formGroup]="createForm" class="flex flex-col gap-3">
<mat-form-field>
<mat-label>Name</mat-label>
<input type="text" matInput formControlName="name" maxlength="100">
@if ((createForm.controls.name.value || '').length > 20) {
<mat-hint>{{ (createForm.controls.name.value || '').length }} / 100 Zeichen</mat-hint>
} @else {
<mat-hint>Wie soll der Schlüssel heißen?</mat-hint>
}
</mat-form-field>
</form>
</mat-dialog-content>
<mat-dialog-actions>
<button mat-button mat-dialog-close >Abbrechen</button>
<button mat-button (click)="save()" [disabled]="createForm.disabled">Speichern</button>
</mat-dialog-actions>

View File

@@ -0,0 +1,66 @@
import { ComponentFixture, TestBed } from '@angular/core/testing';
import { NoopAnimationsModule } from '@angular/platform-browser/animations';
import { CreateSystemComponent } from './create.component';
import { ApiService } from '../../../shared/api.service';
import { MatDialogRef, MAT_DIALOG_DATA } from '@angular/material/dialog';
import { of, throwError } from 'rxjs';
import { HotToastService } from '@ngxpert/hot-toast';
import { FormsModule, ReactiveFormsModule } from '@angular/forms';
import { MockApiService } from '../../../../../mocks/services/mock.api.service';
describe('CreateComponent', () => {
let component: CreateSystemComponent;
let fixture: ComponentFixture<CreateSystemComponent>;
let apiService: ApiService;
beforeEach(async () => {
await TestBed.configureTestingModule({
imports: [CreateSystemComponent, NoopAnimationsModule, FormsModule, ReactiveFormsModule],
providers: [
HotToastService,
{ provide: ApiService, useClass: MockApiService },
{
provide: MatDialogRef,
useValue: []
},
{
provide: MAT_DIALOG_DATA,
useValue: []
}
]
})
.compileComponents();
fixture = TestBed.createComponent(CreateSystemComponent);
component = fixture.componentInstance;
apiService = component['api'];
fixture.detectChanges();
});
it('should create', () => {
expect(component).toBeTruthy();
});
it('should call apiService.createSystem when createSystem is called', () => {
expect(apiService.createSystem).not.toHaveBeenCalled();
component.createForm.setValue({ name: 'Test System' });
component.save();
expect(apiService.createSystem).toHaveBeenCalledWith({ name: 'Test System' });
});
it('should handle success response correctly', () => {
jest.spyOn(apiService, 'createSystem').mockReturnValue(of({}));
const toastSpy = jest.spyOn(component['toast'], 'observe');
component.createForm.setValue({ name: 'Test System' });
component.save();
expect(toastSpy).toHaveBeenCalled();
});
it('should handle error response correctly', () => {
jest.spyOn(apiService, 'createSystem').mockReturnValue(throwError(() => new Error('Test Error')));
const toastSpy = jest.spyOn(component['toast'], 'observe');
component.save();
expect(toastSpy).toHaveBeenCalled();
});
});

View File

@@ -0,0 +1,43 @@
import { Component, inject } from '@angular/core';
import { MatDialogModule, MatDialogRef } from '@angular/material/dialog';
import { HotToastService } from '@ngxpert/hot-toast';
import { ApiService } from '../../../shared/api.service';
import { FormGroup, FormControl, Validators, FormsModule, ReactiveFormsModule } from '@angular/forms';
import { MatButtonModule } from '@angular/material/button';
import { MatFormFieldModule } from '@angular/material/form-field';
import { MatInputModule } from '@angular/material/input';
@Component({
selector: 'app-create',
standalone: true,
imports: [MatDialogModule, MatButtonModule, ReactiveFormsModule, FormsModule, MatFormFieldModule, MatInputModule, MatDialogModule],
templateUrl: './create.component.html',
styleUrl: './create.component.scss'
})
export class CreateSystemComponent {
private api: ApiService = inject(ApiService);
private toast: HotToastService = inject(HotToastService);
readonly dialogRef = inject(MatDialogRef<CreateSystemComponent>);
createForm = new FormGroup({
name: new FormControl<string | null>(null, Validators.required),
})
save() {
this.api.createSystem(this.createForm.value as any)
.pipe(
this.toast.observe({
error: 'Konnte nicht angelegt werden...',
loading: 'Speichern...',
success: 'Gespeichert'
}))
.subscribe({
next: (sys) => {
this.dialogRef.close(sys);
},
error: e => {}
});
}
}

View File

@@ -2,4 +2,8 @@
style="width: 100%; height: 100%;"
(gridReady)="onGridReady($event)"
[gridOptions]="gridOptions!"
/>
/>
<div class="floating-btn-container">
<button mat-flat-button class="btn-create mat-elevation-z8" (click)="openCreateSystem()" color="accent" >System anlegen</button>
</div>

View File

@@ -4,11 +4,15 @@ 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 { 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';
@Component({
selector: 'app-system',
standalone: true,
imports: [AgGridAngular],
imports: [AgGridAngular, MatButtonModule, MatDialogModule],
providers: [DatePipe],
templateUrl: './system.component.html',
styleUrl: './system.component.scss'
@@ -16,6 +20,7 @@ import { HELPER } from '../../shared/helper.service';
export class SystemComponent {
private api: ApiService = inject(ApiService);
private datePipe = inject(DatePipe);
private dialog: MatDialog = inject(MatDialog);
gridApi!: GridApi;
@@ -26,6 +31,13 @@ export class SystemComponent {
{ colId: 'name', field: 'name', headerName: 'Name', sort: 'asc', flex: 1},
{ 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)) : '-' },
{
colId: 'actions'
, headerName: 'Aktionen'
, width: 120
, cellRenderer: AgSystemManagerComponent
// , onCellClicked: (event) => { this.deleteKey(event.data.id)}
}
]
}
@@ -44,4 +56,16 @@ export class SystemComponent {
this.loadSystems();
}
openCreateSystem() {
this.dialog.open(CreateSystemComponent).afterClosed().subscribe({
next: sys => {
if (sys) {
let d = [...this.gridApi.getGridOption("rowData") || [], sys];
this.gridApi.setGridOption("rowData", d);
this.loadSystems();
}
}
})
}
}

View File

@@ -0,0 +1 @@
<div class="manage icon-btn-sm" (click)="openManager()" matTooltip="Manager bearbeiten" [matTooltipShowDelay]="600"></div>

View File

@@ -0,0 +1,13 @@
:host {
display: flex;
flex-direction: row;
align-items: center;
justify-content: center;
width: 100%;
height: 100%;
gap: 12px;
}
.manage {
background-image: url('../../../../../assets/img/manager.svg');
}

View File

@@ -0,0 +1,47 @@
import { ComponentFixture, TestBed } from '@angular/core/testing';
import { AgSystemManagerComponent } from './ag-system-manager.component';
import { MatDialogModule } from '@angular/material/dialog';
import { AgGridAngular } from 'ag-grid-angular';
import { ApiService } from '../../../api.service';
import { of } from 'rxjs';
import { HotToastService } from '@ngxpert/hot-toast';
describe('AgSystemManagerComponent', () => {
let component: AgSystemManagerComponent;
let fixture: ComponentFixture<AgSystemManagerComponent>;
let mockApiService: MockApiService;
const mockHotToastService = {
observe: jest.fn().mockImplementation(() => ({
loading: 'speichern...',
success: 'Änderungen gespeichert',
error: 'Änderungen konnten nicht gespeichert werden!',
subscribe: jest.fn().mockReturnValue(of([]))
}))
};
beforeEach(async () => {
mockApiService = new MockApiService();
await TestBed.configureTestingModule({
imports: [AgSystemManagerComponent, AgGridAngular, MatDialogModule],
providers: [
{ provide: ApiService, useValue: mockApiService },
{ provide: HotToastService, useValue: mockHotToastService },
]
})
.compileComponents();
fixture = TestBed.createComponent(AgSystemManagerComponent);
component = fixture.componentInstance;
fixture.detectChanges();
});
it('should create', () => {
expect(component).toBeTruthy();
});
});
class MockApiService {
getSystems = jest.fn();
}

View File

@@ -0,0 +1,55 @@
import { Component, inject } from '@angular/core';
import { MatDialog, MatDialogModule } from '@angular/material/dialog';
import { MatTooltipModule } from '@angular/material/tooltip';
import { HotToastService } from '@ngxpert/hot-toast';
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';
@Component({
selector: 'app-ag-system-manager',
standalone: true,
imports: [MatDialogModule, MatTooltipModule],
templateUrl: './ag-system-manager.component.html',
styleUrl: './ag-system-manager.component.scss'
})
export class AgSystemManagerComponent implements ICellRendererAngularComp {
private api: ApiService = inject(ApiService);
private dialog: MatDialog = inject(MatDialog);
private toast = inject(HotToastService);
params!: ICellRendererParams<any, any, any>;
system: any;
agInit(params: ICellRendererParams<any, any, any>): void {
this.params = params;
this.system = params.data;
}
refresh(params: ICellRendererParams<any, any, any>): boolean {
return false;
}
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: true
})
// ref.afterClosed().subscribe({
// next: n => {
// if (n) {
// this.deleteThisKey();
// }
// }
// })
}
}

View File

@@ -46,6 +46,10 @@ export class ApiService {
return this.http.get<any[]>('api/system');
}
getSystemManagers(id: string): Observable<IUser[]> {
return this.http.get<any[]>(`api/system/${id}/manager`);
}
handoverKey(data: any) {
return this.http.post(`api/key/${data.key.id}/handover`, data);
}

View File

@@ -12,6 +12,7 @@ export class HELPER {
loading: true,
loadingOverlayComponent: AgLoadingComponent,
rowHeight: 54,
}
}
}