Handout management

This commit is contained in:
Bastian Wagner
2024-10-20 14:49:42 +02:00
parent 5e2b900b18
commit aa9abdd512
37 changed files with 754 additions and 67 deletions

View File

@@ -1,16 +1,15 @@
import { HTTP_INTERCEPTORS, HttpClient } from '@angular/common/http';
import { HttpClient } from '@angular/common/http';
import { Component, inject, LOCALE_ID } from '@angular/core';
import { MAT_DATE_LOCALE } from '@angular/material/core';
import { RouterOutlet } from '@angular/router';
@Component({
selector: 'app-root',
standalone: true,
imports: [RouterOutlet,],
providers: [
{ provide: LOCALE_ID, useValue: 'de-DE' },
{ provide: MAT_DATE_LOCALE, useValue: 'de-DE' }
],
templateUrl: './app.component.html',
styleUrl: './app.component.scss'
@@ -18,8 +17,6 @@ import { RouterOutlet } from '@angular/router';
export class AppComponent {
title = 'client';
private http: HttpClient = inject(HttpClient);
constructor() {
@@ -28,9 +25,4 @@ export class AppComponent {
ngOnInit(): void {
}
}

View File

@@ -1,4 +1,4 @@
import { ApplicationConfig, provideZoneChangeDetection } from '@angular/core';
import { ApplicationConfig, importProvidersFrom, provideZoneChangeDetection } from '@angular/core';
import { provideRouter } from '@angular/router';
import { provideHotToastConfig } from '@ngxpert/hot-toast';
@@ -17,5 +17,7 @@ export const appConfig: ApplicationConfig = {
autoClose: true,
dismissible: false,
duration: 5000
}), provideAnimationsAsync()]
}),
provideAnimationsAsync()
]
};

View File

@@ -0,0 +1,77 @@
@if (isLoading) {
<div class="loading-spinner">
<mat-spinner></mat-spinner>
</div>
}
<h2 mat-dialog-title>Übergaben {{ data.name }}</h2>
<mat-dialog-content>
<h6>Historie:</h6>
<table class="handouts">
<tr>
<th>Kunde</th>
<th >Datum</th>
<th>
Übergabe
</th>
</tr>
@if (handovers.length == 0) {
<tr>
<td>-</td>
<td>-</td>
<td>-</td>
</tr>
}
@for (item of handovers; track $index) {
<tr>
<td>{{ item.customer.name }}</td>
<td>{{ item.timestamp | date}}</td>
<td>{{ item.direction == 'out' ? 'Ausgabe' : 'Rückgabe'}}</td>
</tr>
}
</table>
<form [formGroup]="handoverForm" class="flex-column" style="margin-top: 48px;">
<h6>Neue Übergabe anlegen:</h6>
<mat-form-field>
<mat-label>Kunde</mat-label>
<input type="text"
matInput
formControlName="customer"
[matAutocomplete]="auto">
<mat-autocomplete autoActiveFirstOption #auto="matAutocomplete">
@for (option of filteredCustomers | async; track option) {
<mat-option [value]="option.name">{{option.name}}</mat-option>
}
</mat-autocomplete>
<mat-hint>Wähle den Empfänger oder tippe einen neuen Namen ein</mat-hint>
</mat-form-field>
<div style="margin: 24px 0;">
Der Schlüssel wurde
<mat-radio-group formControlName="direction" class="flex-column" style="align-items: flex-start; justify-content: flex-start;">
<mat-radio-button [value]="'out'">Ausgegeben</mat-radio-button>
<mat-radio-button [value]="'return'">Zurückgegeben</mat-radio-button>
</mat-radio-group>
</div>
<mat-form-field>
<mat-label>Datum der Übergabe</mat-label>
<input matInput [matDatepicker]="picker" formControlName="timestamp">
<mat-hint>TT/MM/JJJJ</mat-hint>
<mat-datepicker-toggle matIconSuffix [for]="picker"></mat-datepicker-toggle>
<mat-datepicker #picker></mat-datepicker>
</mat-form-field>
</form>
</mat-dialog-content>
<mat-dialog-actions>
<button mat-button mat-dialog-close color="warn" >Schließen</button>
<button mat-button (click)="save()" [disabled]="handoverForm.invalid">Speichern</button>
</mat-dialog-actions>

View File

@@ -0,0 +1,24 @@
:host {
width: min(calc(100vw - 24px), 700px);
max-height: calc(100vh - 24px);
display: flex;
flex-direction: column;
}
form {
align-items: stretch;
justify-content: stretch;
}
:host {
position: relative;
}
.handouts{
//margin-top: 32px;
width: 100%;
}
th, td {
text-align: start;
}

View File

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

View File

@@ -0,0 +1,193 @@
import { Component, inject, LOCALE_ID } from '@angular/core';
import { ApiService } from '../../../../shared/api.service';
import { IKey } from '../../../../model/interface/key.interface';
import { MatDialogRef, MAT_DIALOG_DATA, MatDialogModule } from '@angular/material/dialog';
import { FormControl, FormGroup, FormsModule, ReactiveFormsModule, Validators } from '@angular/forms';
import { MatDatepickerModule } from '@angular/material/datepicker';
import { MatFormFieldModule } from '@angular/material/form-field';
import { MatInputModule } from '@angular/material/input';
import { MAT_DATE_LOCALE, provideNativeDateAdapter } from '@angular/material/core';
import { MatButtonModule } from '@angular/material/button';
import { CommonModule } from '@angular/common';
import { MatAutocompleteModule } from '@angular/material/autocomplete';
import { map, Observable, startWith, timestamp } from 'rxjs';
import {
MatBottomSheet,
MatBottomSheetModule,
MatBottomSheetRef,
} from '@angular/material/bottom-sheet';
import {MatListModule} from '@angular/material/list';
import {MatProgressSpinnerModule} from '@angular/material/progress-spinner';
import {MatRadioModule} from '@angular/material/radio';
import { HotToastService } from '@ngxpert/hot-toast';
@Component({
selector: 'app-handover-dialog',
standalone: true,
imports: [FormsModule, ReactiveFormsModule, MatDatepickerModule, MatFormFieldModule, MatInputModule, MatButtonModule, MatDialogModule, CommonModule, MatAutocompleteModule, MatProgressSpinnerModule, MatRadioModule],
providers: [
provideNativeDateAdapter(),
{ provide: LOCALE_ID, useValue: 'de-DE' },
{ provide: MAT_DATE_LOCALE, useValue: 'de-DE' },
],
templateUrl: './handover-dialog.component.html',
styleUrl: './handover-dialog.component.scss'
})
export class HandoverDialogComponent {
private api: ApiService = inject(ApiService);
readonly dialogRef = inject(MatDialogRef<HandoverDialogComponent>);
readonly data = inject<IKey>(MAT_DIALOG_DATA);
private _bottomSheet = inject(MatBottomSheet);
private toast: HotToastService = inject(HotToastService);
isLoading: boolean = false;
customers: { name: string, id: string }[] = [];
filteredCustomers: Observable<any[]> = new Observable();
handovers: any[] = [];
handoverForm = new FormGroup({
customer: new FormControl<any>(null, Validators.required),
key: new FormControl(this.data),
direction: new FormControl('out', Validators.required),
timestamp: new FormControl(new Date(), Validators.required)
});
ngOnInit() {
this.loadData();
}
loadData() {
this.isLoading = true;
const promises: Observable<any>[] = [
this.getHandovers(),
this.loadCustomers()
];
Promise.all(promises).then(() => {
this.isLoading = false;
})
}
getHandovers() {
const promise = this.api.getHandovers(this.data.id)
promise.subscribe({
next: n => {
this.handovers = n;
if (n && n.length > 0) {
this.handoverForm.controls.customer.patchValue(n[0].customer.name);
this.handoverForm.controls.direction.patchValue(n[0].direction == 'out' ? 'return' : 'out')
}
}
});
return promise;
}
loadCustomers() {
const promise = this.api.getCustomers()
promise.subscribe({
next: customers => {
this.customers = customers;
this.filteredCustomers = this.handoverForm.controls.customer.valueChanges.pipe(
startWith(''),
map(value => this._filter(value || '')),
);
}
});
return promise;
}
private _filter(value: string): any[] {
const filterValue = value.toLowerCase();
return this.customers.filter(option => option.name.toLowerCase().includes(filterValue) || option.id.toLowerCase().includes(filterValue));
}
save() {
const val = this.handoverForm.value;
const dto = {
key: this.data,
customer: this.customers.find(c => c.name == val.customer || c.id == val.customer),
timestamp: val.timestamp,
direction: val.direction
}
if (dto.customer == null) {
this._bottomSheet.open(BottomSheetCreateCustomer).afterDismissed().subscribe({
next: async n => {
if (!n) { return; }
await this.createCustomer(val.customer);
this.saveIt(dto);
}
})
} else {
this.saveIt(dto);
}
}
saveIt(data: any) {
this.api.handoverKey(data)
.pipe(
this.toast.observe({
loading: 'Speichern...',
error: 'Konnte nicht gespeichert werden. Bitte versuche es später erneut',
success: 'Gespeichert'
})
)
.subscribe({
next: n => {
this.dialogRef.close(data.direction == 'out')
}
})
}
createCustomer(name: string) {
this.isLoading = true;
this.api.createCustomer({ name, system: this.data.cylinder.system}).subscribe({
next: n => {
this.isLoading = false;
}
})
}
}
@Component({
template: `
<mat-nav-list>
<a mat-list-item (click)="openLink($event, true)">
<span matListItemTitle>Anlegen</span>
<span matLine>Neuen Empfänger anlegen</span>
</a>
<a mat-list-item (click)="openLink($event)">
<span matListItemTitle>Abbrechen</span>
<span matLine>Zurück zur Auswahl</span>
</a>
</mat-nav-list>
`,
standalone: true,
imports: [MatInputModule, MatListModule],
})
export class BottomSheetCreateCustomer {
private _bottomSheetRef =
inject<MatBottomSheetRef<BottomSheetCreateCustomer>>(MatBottomSheetRef);
openLink(event: MouseEvent, data?: boolean): void {
this._bottomSheetRef.dismiss(data);
event.preventDefault();
}
}

View File

@@ -9,6 +9,7 @@ import { HotToastService } from '@ngxpert/hot-toast';
import { MatButtonModule } from '@angular/material/button';
import { MatDialog, MatDialogModule } from '@angular/material/dialog';
import { CreateKeyComponent } from './create/create.component';
import { AgOpenHandoutComponent } from '../../shared/ag-grid/components/ag-open-handout/ag-open-handout.component';
@Component({
selector: 'app-keys',
@@ -32,7 +33,12 @@ export class KeysComponent {
localeText: AG_GRID_LOCALE_DE,
rowData: [],
columnDefs: [
{ field: 'handedOut' , headerName: 'Ausgegeben', width: 100,editable: true, filter: true, headerTooltip: 'Ausgegeben' },
{
cellRenderer: AgOpenHandoutComponent,
width: 100,
headerName: 'Übergabe'
},
{ field: 'handedOut' , headerName: 'Ausgegeben', width: 100, editable: false, filter: false, headerTooltip: 'Ausgegeben' },
{ field: 'name' , headerName: 'Name', flex: 1, editable: true, sort: 'asc', filter: true },
{ field: 'nr' , headerName: 'Schlüsselnummer', flex: 1, editable: true, filter: true },
{ field: 'cylinder' , headerName: 'Zylinder', flex: 1, editable: true, filter: true, cellRenderer: (data: any) => {return data.value?.name}, cellEditor: 'agSelectCellEditor',
@@ -60,9 +66,10 @@ export class KeysComponent {
, type: 'date'
, cellRenderer: (data: any) => data.value ? this.datePipe.transform(new Date(data.value)) : '-'
, tooltipValueGetter: (data: any) => this.datePipe.transform(new Date(data.value), 'medium')
},
}
],
loading: true,
rowHeight: 48,
}
ngOnInit(): void {
@@ -71,7 +78,6 @@ export class KeysComponent {
this.cylinders = n;
}
})
this.api.postKeySystem({ name: 'Development' }).subscribe()
}
loadKeys() {
@@ -79,7 +85,6 @@ export class KeysComponent {
this.api.getKeys().subscribe(res => {
this.gridApi.setGridOption("rowData", res);
this.gridApi.setGridOption("loading", false);
res.map((r: any) => console.log(r.updatedAt))
})
}
@@ -91,8 +96,6 @@ export class KeysComponent {
cellEditEnd(event: CellEditingStoppedEvent) {
const key: IKey = event.data;
console.log(event)
if (!event.valueChanged || event.newValue == event.oldValue) { return; }
this.gridApi.setGridOption("loading", true);

View File

@@ -0,0 +1 @@
<div class="handover icon-btn-sm" (click)="openDialog()"></div>

View File

@@ -0,0 +1,11 @@
:host {
display: flex;
align-items: center;
justify-content: center;
flex-direction: column;
height: 100%;
}
.handover {
background-image: url('../../../../../assets/img/handover.svg');
}

View File

@@ -0,0 +1,44 @@
import { Component, HostBinding, inject } from '@angular/core';
import { ICellRendererAngularComp } from 'ag-grid-angular';
import { ICellRendererParams } from 'ag-grid-community';
import { IKey } from '../../../../model/interface/key.interface';
import { MatDialog, MatDialogModule } from '@angular/material/dialog';
import { HandoverDialogComponent } from '../../../../modules/keys/components/handover-dialog/handover-dialog.component';
@Component({
selector: 'app-ag-open-handout',
standalone: true,
imports: [MatDialogModule],
templateUrl: './ag-open-handout.component.html',
styleUrl: './ag-open-handout.component.scss'
})
export class AgOpenHandoutComponent implements ICellRendererAngularComp {
private dialog: MatDialog = inject(MatDialog);
key!: IKey;
params!: ICellRendererParams<any, any, any>;
agInit(params: ICellRendererParams<any, any, any>): void {
this.params = params;
this.key = params.data;
}
refresh(params: ICellRendererParams<any, any, any>): boolean {
return false;
}
openDialog() {
this.dialog.open(HandoverDialogComponent, {
data: this.key,
autoFocus: false,
maxWidth: '100vw',
maxHeight: '100vh'
}).afterClosed().subscribe({
next: n => {
if (n != null) {
this.key.handedOut = n;
this.params.api.refreshCells();
}
}
})
}
}

View File

@@ -43,4 +43,21 @@ export class ApiService {
postKeySystem(keySystem: any) {
return this.http.post('api/key/system', keySystem);
}
handoverKey(data: any) {
return this.http.post(`api/key/${data.key.id}/handover`, data);
}
getHandovers(keyID: string): Observable<any[]> {
return this.http.get<any[]>(`api/key/${keyID}/handover`);
}
createCustomer(data: { name: string, system: any}) {
return this.http.post('api/customer', data);
}
getCustomers(): Observable<any[]> {
return this.http.get<any[]>('api/customer')
}
}

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 5.1 KiB

View File

@@ -8,6 +8,50 @@ html, body {
background-color: #e2e2e2;
}
.flex {
display: flex;
align-items: center;
justify-content: center;
}
.flex-column {
display: flex;
align-items: center;
justify-content: center;
flex-direction: column;
}
.icon-btn-sm {
box-shadow: 0 0 0 4px transparent,0 1px 2px #0c111d11;
border: 1px solid #d0d5dd;
background-color: white;
cursor: pointer;
padding: 4px;
box-sizing: border-box;
border-radius: 6px;
background-size: 28px;
background-position: center;
background-repeat: no-repeat;
width: 38px;
height: 38px;
}
.loading-spinner {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
background-color: rgba(255, 255, 255, 0.4);
z-index: 1000;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
}
/* Core Data Grid CSS */
@import "ag-grid-community/styles/ag-grid.css";
/* Quartz Theme Specific CSS */