Multicylinders
This commit is contained in:
@@ -4,6 +4,7 @@ import {
|
|||||||
CreateDateColumn,
|
CreateDateColumn,
|
||||||
DeleteDateColumn,
|
DeleteDateColumn,
|
||||||
Entity,
|
Entity,
|
||||||
|
ManyToMany,
|
||||||
ManyToOne,
|
ManyToOne,
|
||||||
OneToMany,
|
OneToMany,
|
||||||
PrimaryGeneratedColumn,
|
PrimaryGeneratedColumn,
|
||||||
@@ -20,7 +21,7 @@ export class Cylinder {
|
|||||||
@Column({ nullable: false, unique: true })
|
@Column({ nullable: false, unique: true })
|
||||||
name: string;
|
name: string;
|
||||||
|
|
||||||
@OneToMany(() => Key, (key) => key.cylinder)
|
@ManyToMany(() => Key, (key) => key.cylinder)
|
||||||
keys: Key[];
|
keys: Key[];
|
||||||
|
|
||||||
@ManyToOne(() => KeySystem, (sys) => sys.cylinders)
|
@ManyToOne(() => KeySystem, (sys) => sys.cylinders)
|
||||||
|
|||||||
@@ -3,6 +3,8 @@ import {
|
|||||||
CreateDateColumn,
|
CreateDateColumn,
|
||||||
DeleteDateColumn,
|
DeleteDateColumn,
|
||||||
Entity,
|
Entity,
|
||||||
|
JoinTable,
|
||||||
|
ManyToMany,
|
||||||
ManyToOne,
|
ManyToOne,
|
||||||
PrimaryGeneratedColumn,
|
PrimaryGeneratedColumn,
|
||||||
UpdateDateColumn,
|
UpdateDateColumn,
|
||||||
@@ -28,8 +30,9 @@ export class Key implements IKey {
|
|||||||
@Column({ name: 'lost', default: false })
|
@Column({ name: 'lost', default: false })
|
||||||
keyLost: boolean;
|
keyLost: boolean;
|
||||||
|
|
||||||
@ManyToOne(() => Cylinder, (cylinder) => cylinder.keys)
|
@ManyToMany(() => Cylinder, (cylinder) => cylinder.keys)
|
||||||
cylinder: Cylinder;
|
@JoinTable()
|
||||||
|
cylinder: Cylinder[];
|
||||||
|
|
||||||
@ManyToOne(() => Customer, (customer) => customer.keys)
|
@ManyToOne(() => Customer, (customer) => customer.keys)
|
||||||
customer: Customer;
|
customer: Customer;
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ import {
|
|||||||
Column,
|
Column,
|
||||||
CreateDateColumn,
|
CreateDateColumn,
|
||||||
Entity,
|
Entity,
|
||||||
|
ManyToMany,
|
||||||
ManyToOne,
|
ManyToOne,
|
||||||
PrimaryGeneratedColumn,
|
PrimaryGeneratedColumn,
|
||||||
} from 'typeorm';
|
} from 'typeorm';
|
||||||
@@ -27,8 +28,8 @@ export class KeyActivity implements IKey {
|
|||||||
@Column({ name: 'handed_out', default: false })
|
@Column({ name: 'handed_out', default: false })
|
||||||
handedOut: boolean;
|
handedOut: boolean;
|
||||||
|
|
||||||
@ManyToOne(() => Cylinder)
|
@ManyToMany(() => Cylinder)
|
||||||
cylinder: Cylinder;
|
cylinder: Cylinder[];
|
||||||
|
|
||||||
@ManyToOne(() => Customer)
|
@ManyToOne(() => Customer)
|
||||||
customer: Customer;
|
customer: Customer;
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ export interface IKey {
|
|||||||
name: string;
|
name: string;
|
||||||
nr: string;
|
nr: string;
|
||||||
handedOut: boolean;
|
handedOut: boolean;
|
||||||
cylinder: Cylinder;
|
cylinder: Cylinder[];
|
||||||
customer: Customer;
|
customer: Customer;
|
||||||
createdAt: Date;
|
createdAt: Date;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,10 +2,16 @@ import { map } from "rxjs";
|
|||||||
|
|
||||||
jest.mock('@ngxpert/hot-toast', () => ({
|
jest.mock('@ngxpert/hot-toast', () => ({
|
||||||
HotToastService: {
|
HotToastService: {
|
||||||
observe: jest.fn().mockImplementation(() => map(x => x))
|
observe: jest.fn().mockImplementation(() => map(x => x)),
|
||||||
|
info: jest.fn(),
|
||||||
|
error: jest.fn(),
|
||||||
|
success: jest.fn(),
|
||||||
},
|
},
|
||||||
}));
|
}));
|
||||||
|
|
||||||
export class MockHotToastService {
|
export class MockHotToastService {
|
||||||
observe = jest.fn().mockImplementation(() => map(x => x))
|
observe = jest.fn().mockImplementation(() => map(x => x));
|
||||||
|
info = jest.fn();
|
||||||
|
error = jest.fn();
|
||||||
|
success = jest.fn();
|
||||||
};
|
};
|
||||||
@@ -18,18 +18,23 @@
|
|||||||
<mat-hint>Nummer auf dem Schlüssel</mat-hint>
|
<mat-hint>Nummer auf dem Schlüssel</mat-hint>
|
||||||
</mat-form-field>
|
</mat-form-field>
|
||||||
|
|
||||||
<mat-form-field>
|
<div class="flex items-center gap-3">
|
||||||
|
<mat-form-field class="flex-auto">
|
||||||
<mat-label>Schließzylinder</mat-label>
|
<mat-label>Schließzylinder</mat-label>
|
||||||
<mat-select formControlName="cylinder">
|
<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 }}</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()">
|
||||||
|
<mat-icon>open_in_new</mat-icon>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
</form>
|
</form>
|
||||||
</mat-dialog-content>
|
</mat-dialog-content>
|
||||||
<mat-dialog-actions>
|
<mat-dialog-actions>
|
||||||
<button mat-button mat-dialog-close >Abbrechen</button>
|
<button mat-button mat-dialog-close >Abbrechen</button>
|
||||||
<button mat-button (click)="save()" [disabled]="createForm.disabled">Speichern</button>
|
<button mat-button (click)="save()" [disabled]="createForm.disabled || createForm.invalid">Speichern</button>
|
||||||
</mat-dialog-actions>
|
</mat-dialog-actions>
|
||||||
@@ -1,7 +1,7 @@
|
|||||||
import { Component, inject } from '@angular/core';
|
import { Component, inject } from '@angular/core';
|
||||||
import { FormControl, FormGroup, FormsModule, ReactiveFormsModule, Validators } from '@angular/forms';
|
import { FormControl, FormGroup, FormsModule, ReactiveFormsModule, Validators } from '@angular/forms';
|
||||||
import { MatButtonModule } from '@angular/material/button';
|
import { MatButtonModule } from '@angular/material/button';
|
||||||
import { MatDialogModule, MatDialogRef } from '@angular/material/dialog';
|
import { MatDialog, MatDialogModule, MatDialogRef } from '@angular/material/dialog';
|
||||||
import { ApiService } from '../../../shared/api.service';
|
import { ApiService } from '../../../shared/api.service';
|
||||||
import { MatAutocompleteModule } from '@angular/material/autocomplete';
|
import { MatAutocompleteModule } from '@angular/material/autocomplete';
|
||||||
import { MatFormFieldModule } from '@angular/material/form-field';
|
import { MatFormFieldModule } from '@angular/material/form-field';
|
||||||
@@ -9,11 +9,13 @@ import { MatInputModule } from '@angular/material/input';
|
|||||||
import { map, Observable, startWith } from 'rxjs';
|
import { map, Observable, startWith } from 'rxjs';
|
||||||
import { MatSelectModule } from '@angular/material/select';
|
import { MatSelectModule } from '@angular/material/select';
|
||||||
import { HotToastService } from '@ngxpert/hot-toast';
|
import { HotToastService } from '@ngxpert/hot-toast';
|
||||||
|
import { SelectKeyCylinderComponent } from './select-key-cylinder/select-key-cylinder.component';
|
||||||
|
import { MatIconModule } from '@angular/material/icon';
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
selector: 'app-create',
|
selector: 'app-create',
|
||||||
standalone: true,
|
standalone: true,
|
||||||
imports: [MatDialogModule, MatButtonModule, ReactiveFormsModule, FormsModule, MatFormFieldModule, MatInputModule, MatSelectModule, MatDialogModule],
|
imports: [MatDialogModule, MatButtonModule, ReactiveFormsModule, FormsModule, MatFormFieldModule, MatInputModule, MatSelectModule, MatDialogModule, MatIconModule],
|
||||||
templateUrl: './create.component.html',
|
templateUrl: './create.component.html',
|
||||||
styleUrl: './create.component.scss'
|
styleUrl: './create.component.scss'
|
||||||
})
|
})
|
||||||
@@ -22,6 +24,7 @@ export class CreateKeyComponent {
|
|||||||
private api: ApiService = inject(ApiService);
|
private api: ApiService = inject(ApiService);
|
||||||
private toast: HotToastService = inject(HotToastService);
|
private toast: HotToastService = inject(HotToastService);
|
||||||
readonly dialogRef = inject(MatDialogRef<CreateKeyComponent>);
|
readonly dialogRef = inject(MatDialogRef<CreateKeyComponent>);
|
||||||
|
private readonly dialog = inject(MatDialog);
|
||||||
|
|
||||||
createForm = new FormGroup({
|
createForm = new FormGroup({
|
||||||
name: new FormControl(null, Validators.required),
|
name: new FormControl(null, Validators.required),
|
||||||
@@ -57,6 +60,8 @@ export class CreateKeyComponent {
|
|||||||
}
|
}
|
||||||
|
|
||||||
save() {
|
save() {
|
||||||
|
console.log(this.createForm.value)
|
||||||
|
|
||||||
this.api.createKey(this.createForm.value as any)
|
this.api.createKey(this.createForm.value as any)
|
||||||
.pipe(
|
.pipe(
|
||||||
this.toast.observe({
|
this.toast.observe({
|
||||||
@@ -72,4 +77,23 @@ export class CreateKeyComponent {
|
|||||||
}
|
}
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
openSelectMultipleCylinders() {
|
||||||
|
this.dialog.open(SelectKeyCylinderComponent, {
|
||||||
|
maxHeight: "calc(100vh - 24px)",
|
||||||
|
maxWidth: "calc(100vw - 24px)",
|
||||||
|
width: "50vw",
|
||||||
|
minWidth: "300px",
|
||||||
|
height: "70vh",
|
||||||
|
disableClose: true,
|
||||||
|
data: this.cylinders
|
||||||
|
}).afterClosed().subscribe({
|
||||||
|
next: c => {
|
||||||
|
if (c) {
|
||||||
|
this.createForm.controls.cylinder.patchValue(c);
|
||||||
|
console.log(c);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,15 @@
|
|||||||
|
|
||||||
|
|
||||||
|
<h2 mat-dialog-title>Zylinder auswählen</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 color="warn" [mat-dialog-close]="null">Abbrechen</button>
|
||||||
|
<button mat-button color="accent" [mat-dialog-close]="selectedCylinders">Übernehmen</button>
|
||||||
|
</mat-dialog-actions>
|
||||||
@@ -0,0 +1,37 @@
|
|||||||
|
import { ComponentFixture, TestBed } from '@angular/core/testing';
|
||||||
|
|
||||||
|
import { SelectKeyCylinderComponent } from './select-key-cylinder.component';
|
||||||
|
import { HotToastService } from '@ngxpert/hot-toast';
|
||||||
|
import { MAT_DIALOG_DATA, MatDialogRef } from '@angular/material/dialog';
|
||||||
|
import { MockHotToastService } from '../../../../../../mocks/services/mock.hottoast.service';
|
||||||
|
|
||||||
|
describe('SelectKeyCylinderComponent', () => {
|
||||||
|
let component: SelectKeyCylinderComponent;
|
||||||
|
let fixture: ComponentFixture<SelectKeyCylinderComponent>;
|
||||||
|
|
||||||
|
const mockHotToastService = {
|
||||||
|
info: jest.fn(),
|
||||||
|
error: jest.fn(),
|
||||||
|
success: jest.fn(),
|
||||||
|
}
|
||||||
|
|
||||||
|
beforeEach(async () => {
|
||||||
|
await TestBed.configureTestingModule({
|
||||||
|
imports: [SelectKeyCylinderComponent],
|
||||||
|
providers: [
|
||||||
|
{ provide: HotToastService, useClass: MockHotToastService },
|
||||||
|
{ provide: MatDialogRef, useValue: {} },
|
||||||
|
{ provide: MAT_DIALOG_DATA, useValue: [] }
|
||||||
|
]
|
||||||
|
})
|
||||||
|
.compileComponents();
|
||||||
|
|
||||||
|
fixture = TestBed.createComponent(SelectKeyCylinderComponent);
|
||||||
|
component = fixture.componentInstance;
|
||||||
|
fixture.detectChanges();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should create', () => {
|
||||||
|
expect(component).toBeTruthy();
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -0,0 +1,54 @@
|
|||||||
|
import { Component, inject } from '@angular/core';
|
||||||
|
import { ApiService } from '../../../../shared/api.service';
|
||||||
|
import { ICylinder } from '../../../../model/interface/cylinder.interface';
|
||||||
|
import { HotToastService } from '@ngxpert/hot-toast';
|
||||||
|
import { AgGridAngular } from 'ag-grid-angular';
|
||||||
|
import { GridApi, GridOptions, GridReadyEvent } from 'ag-grid-community';
|
||||||
|
import { AG_GRID_LOCALE_DE } from '@ag-grid-community/locale';
|
||||||
|
import { MAT_DIALOG_DATA, MatDialogModule, MatDialogRef } from '@angular/material/dialog';
|
||||||
|
import { MatButtonModule } from '@angular/material/button';
|
||||||
|
|
||||||
|
@Component({
|
||||||
|
selector: 'app-select-key-cylinder',
|
||||||
|
standalone: true,
|
||||||
|
imports: [AgGridAngular, MatDialogModule, MatButtonModule],
|
||||||
|
templateUrl: './select-key-cylinder.component.html',
|
||||||
|
styleUrl: './select-key-cylinder.component.scss'
|
||||||
|
})
|
||||||
|
export class SelectKeyCylinderComponent {
|
||||||
|
private toast: HotToastService = inject(HotToastService);
|
||||||
|
readonly dialogRef = inject(MatDialogRef<SelectKeyCylinderComponent>);
|
||||||
|
readonly cylinders = inject<ICylinder[]>(MAT_DIALOG_DATA);
|
||||||
|
|
||||||
|
gridApi!: GridApi;
|
||||||
|
|
||||||
|
gridOptions: GridOptions = {
|
||||||
|
localeText: AG_GRID_LOCALE_DE,
|
||||||
|
loading: false,
|
||||||
|
rowData: this.cylinders,
|
||||||
|
onSelectionChanged: (event) => {
|
||||||
|
this.selectionChanged();
|
||||||
|
},
|
||||||
|
rowSelection: 'multiple',
|
||||||
|
columnDefs: [
|
||||||
|
// selected rows
|
||||||
|
{ colId: 'selected', headerName: '', checkboxSelection: true, width: 40, headerCheckboxSelection: true, headerCheckboxSelectionFilteredOnly: true },
|
||||||
|
{ colId: 'name', field: 'name' , headerName: 'Name', flex: 1, editable: true, sort: 'asc', filter: true },
|
||||||
|
]
|
||||||
|
};
|
||||||
|
|
||||||
|
selectedCylinders: ICylinder[] = [];
|
||||||
|
|
||||||
|
ngOnInit(): void {
|
||||||
|
console.log(this.toast)
|
||||||
|
this.toast.error('Wähle die Zylinder aus, die dem Schlüssel zugeordnet werden sollen.');
|
||||||
|
}
|
||||||
|
|
||||||
|
onGridReady(params: GridReadyEvent) {
|
||||||
|
this.gridApi = params.api;
|
||||||
|
}
|
||||||
|
|
||||||
|
selectionChanged(): void {
|
||||||
|
this.selectedCylinders =this.gridApi?.getSelectedRows();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -14,6 +14,7 @@ import { MatIconModule } from '@angular/material/icon';
|
|||||||
import { ArchiveComponent } from './components/archive/archive.component';
|
import { ArchiveComponent } from './components/archive/archive.component';
|
||||||
import { AgLoadingComponent } from '../../shared/ag-grid/components/ag-loading/ag-loading.component';
|
import { AgLoadingComponent } from '../../shared/ag-grid/components/ag-loading/ag-loading.component';
|
||||||
import { map, of } from 'rxjs';
|
import { map, of } from 'rxjs';
|
||||||
|
import { ICylinder } from '../../model/interface/cylinder.interface';
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
selector: 'app-keys',
|
selector: 'app-keys',
|
||||||
@@ -40,18 +41,21 @@ export class KeysComponent {
|
|||||||
|
|
||||||
{ colId: 'name', field: 'name' , headerName: 'Name', flex: 1, editable: true, sort: 'asc', filter: true },
|
{ 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 },
|
{ colId: 'nr', field: 'nr' , headerName: 'Schlüsselnummer', flex: 1, editable: true, filter: true },
|
||||||
{ colId: 'cylinder', field: 'cylinder' , headerName: 'Zylinder', flex: 1, editable: true, filter: true, cellRenderer: (data: any) => {return data.value?.name}, cellEditor: 'agSelectCellEditor',
|
{ colId: 'cylinder', field: 'cylinder' , headerName: 'Zylinder', flex: 1, editable: false, filter: true, cellRenderer: (data: any) => {return data.value?.map((m: ICylinder) => m.name).join(', ')}, cellEditor: 'agSelectCellEditor',
|
||||||
cellEditorParams: () => {
|
// cellEditorParams: () => {
|
||||||
return {
|
// return {
|
||||||
values: this.cylinders,
|
// values: this.cylinders,
|
||||||
}
|
// }
|
||||||
},
|
// },
|
||||||
valueFormatter: (val) => {
|
// valueFormatter: (val) => {
|
||||||
return val.value?.name + ` (${val.value?.system?.name})`;
|
// return val.value?.name + ` (${val.value?.system?.name})`;
|
||||||
},
|
// },
|
||||||
cellEditorPopup: false
|
cellEditorPopup: false
|
||||||
},
|
},
|
||||||
{ colId: 'system', field: 'cylinder.system' , headerName: 'Schließanlage', flex: 1, editable: false, filter: true, cellRenderer: (data: any) => {return data.value?.name} },
|
{ colId: 'system', field: 'cylinder' , headerName: 'Schließanlage', flex: 1, editable: false, filter: true, cellRenderer: (data: any) => {
|
||||||
|
const s = new Set<string>(data.value?.map((m: ICylinder) => m.system?.name));
|
||||||
|
return [...s].join(', ')
|
||||||
|
} },
|
||||||
{ colId: 'customer', field: 'customer' , headerName: 'Kunde', 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'
|
field: 'createdAt'
|
||||||
@@ -92,15 +96,25 @@ export class KeysComponent {
|
|||||||
this.api.getCylinders().subscribe({
|
this.api.getCylinders().subscribe({
|
||||||
next: n => {
|
next: n => {
|
||||||
this.cylinders = n;
|
this.cylinders = n;
|
||||||
|
},
|
||||||
|
error: () => {
|
||||||
|
this.cylinders = [];
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
loadKeys() {
|
loadKeys() {
|
||||||
this.gridApi.setGridOption("loading", true);
|
this.gridApi.setGridOption("loading", true);
|
||||||
this.api.getKeys().subscribe(res => {
|
this.api.getKeys().subscribe({
|
||||||
this.gridApi.setGridOption("rowData", res);
|
next: n => {
|
||||||
|
this.gridApi.setGridOption("rowData", n);
|
||||||
this.gridApi.setGridOption("loading", false);
|
this.gridApi.setGridOption("loading", false);
|
||||||
|
},
|
||||||
|
error: () => {
|
||||||
|
this.gridApi.setGridOption("loading", false);
|
||||||
|
// set error in grid
|
||||||
|
this.toast.error('Fehler beim Laden der Schlüssel')
|
||||||
|
}
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -131,7 +145,12 @@ export class KeysComponent {
|
|||||||
}
|
}
|
||||||
|
|
||||||
openCreateKey() {
|
openCreateKey() {
|
||||||
this.dialog.open(CreateKeyComponent).afterClosed().subscribe({
|
this.dialog.open(CreateKeyComponent, {
|
||||||
|
maxWidth: "calc(100vw - 24px)",
|
||||||
|
width: "30vw",
|
||||||
|
minWidth: "200px",
|
||||||
|
disableClose: true
|
||||||
|
}).afterClosed().subscribe({
|
||||||
next: key => {
|
next: key => {
|
||||||
if (key) {
|
if (key) {
|
||||||
let d = [...this.gridApi.getGridOption("rowData") || [], key];
|
let d = [...this.gridApi.getGridOption("rowData") || [], key];
|
||||||
|
|||||||
Reference in New Issue
Block a user