No changes detected in diff

No code was added or removed in the provided diff; only context lines were present.
This commit is contained in:
2026-02-02 10:14:15 +01:00
parent 75846573da
commit eda30472b9
97 changed files with 7 additions and 28 deletions

View File

@@ -0,0 +1,9 @@
<mat-form-field class="example-form-field">
<mat-label>{{label}}</mat-label>
<input matInput type="text" [(ngModel)]="value">
@if (value) {
<button matSuffix mat-icon-button aria-label="Clear" (click)="value=''">
<mat-icon>close</mat-icon>
</button>
}
</mat-form-field>

View File

@@ -0,0 +1,7 @@
.mat-stepper-vertical {
margin-top: 8px;
}
.mat-mdc-form-field {
margin-top: 16px;
}

View File

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

View File

@@ -0,0 +1,18 @@
import {Component, Input} from '@angular/core';
import {MatIconModule} from '@angular/material/icon';
import {MatButtonModule} from '@angular/material/button';
import {FormsModule} from '@angular/forms';
import {MatInputModule} from '@angular/material/input';
import {MatFormFieldModule} from '@angular/material/form-field';
@Component({
selector: 'clearable-input',
standalone: true,
imports: [MatFormFieldModule, MatInputModule, FormsModule, MatButtonModule, MatIconModule],
templateUrl: './clearable-input.component.html',
styleUrl: './clearable-input.component.scss'
})
export class ClearableInputComponent {
@Input() public value: string = '';
@Input() public label: string = '';
}

View File

@@ -0,0 +1,25 @@
<!-- src/app/components/login/login.component.html -->
<form [formGroup]="loginForm" (ngSubmit)="onSubmit()">
<div class="mb-3">
<mat-form-field>
<mat-label>Username</mat-label>
<input matInput formControlName="username">
</mat-form-field>
</div>
<div class="mb-3">
<mat-form-field>
<mat-label>Enter your password</mat-label>
<input matInput [type]="hide ? 'password' : 'text'" formControlName="password">
<button type="button" mat-icon-button matSuffix (click)="togglePWVisibility($event)"
[attr.aria-label]="'Hide password'" [attr.aria-pressed]="hide">
<mat-icon>{{hide ? 'visibility_off' : 'visibility'}}</mat-icon>
</button>
</mat-form-field>
</div>
<div class="mb-3">
<button type="submit" class="btn" mat-flat-button color="primary">
<span class="me-2">Anmeldung</span>
<span class="p-0 m-0" [class.spinner-border]="wait4waitRes" [class.spinner-border-sm]="wait4waitRes" aria-hidden="true"></span>
</button>
</div>
</form>

View File

@@ -0,0 +1,3 @@
mat-form-field{
width: 100%;
}

View File

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

View File

@@ -0,0 +1,49 @@
import { Component } from '@angular/core';
import { MatIconModule } from '@angular/material/icon';
import { MatButtonModule } from '@angular/material/button';
import { MatInputModule } from '@angular/material/input';
import { MatFormFieldModule } from '@angular/material/form-field';
import { FormBuilder, FormGroup, Validators } from '@angular/forms';
import { AuthService } from '../../services/auth.service'
import { Router } from '@angular/router';
import { ReactiveFormsModule } from '@angular/forms';
@Component({
selector: 'login',
standalone: true,
imports: [MatFormFieldModule, MatInputModule, MatButtonModule, MatIconModule, ReactiveFormsModule],
templateUrl: './login.component.html',
styleUrl: './login.component.scss'
})
export class LoginComponent {
hide = true;
loginForm: FormGroup;
wait4waitRes: boolean = false;
constructor(private fb: FormBuilder, private authService: AuthService, private router: Router) {
this.loginForm = this.fb.group({
username: ['', Validators.required],
password: ['', Validators.required]
});
}
togglePWVisibility(event: MouseEvent) {
this.hide = !this.hide;
event.stopPropagation();
}
onSubmit() {
if (this.loginForm.valid) {
this.wait4waitRes = true;
this.authService.login(this.loginForm.value).subscribe({
next: () => {
this.wait4waitRes = false;
this.router.navigate(['/envelope']);
},
error: error => {
this.wait4waitRes = false;
}
});
}
}
}

View File

@@ -0,0 +1,40 @@
<header>
<nav class="navbar navbar-expand-lg bg-body-tertiary fs-5">
<div class="container-fluid">
<a class="navbar-brand fs-2 fw-bold" [routerLink]="['/']">signFlow</a>
<!-- Navbars -->
<div *ngIf="isLogedIn()" class="navbar-collapse collapse d-sm-inline-flex justify-content-center"
[ngClass]="{ show: isExpanded }">
<ul class=" navbar-nav flex-grow">
<li class="nav-item" [routerLinkActive]="['link-active']" [routerLinkActiveOptions]="{exact: true}">
<button class="btn nav-link d-inline-flex flex-column align-items-center justify-content-center mx-2" (click)="router.navigate(['/'])">
<mat-icon>mail</mat-icon>
<span class="fs-6">Umschläge</span>
</button>
<button class="btn nav-link d-inline-flex flex-column align-items-center justify-content-center mx-2" (click)="router.navigate(['/envelope-creation'])">
<mat-icon>forward_to_inbox</mat-icon>
<span class="fs-6">Neuer Umschlag</span>
</button>
</li>
</ul>
</div>
<!-- Right menu -->
<div class="navbar-collapse justify-content-end me-5">
<button class="navbar-toggler" type="button" data-toggle="collapse" data-target=".navbar-collapse"
aria-label="Toggle navigation" [attr.aria-expanded]="isExpanded" (click)="toggle()">
<span class="navbar-toggler-icon"></span>
</button>
<button class="fs-5 btn d-flex align-items-center ms-2 me-0 pe-0" type="button" (click)="logInOut()">
@if(isLogedIn()){
<mat-icon>logout</mat-icon>
<span class="fs-6 ms-1">Abmelden</span>
}
@else {
<mat-icon>login</mat-icon>
<span class="fs-6 ms-1">Anmelden</span>
}
</button>
</div>
</div>
</nav>
</header>

View File

@@ -0,0 +1,3 @@
mat-toolbar{
height: 4rem
}

View File

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

View File

@@ -0,0 +1,55 @@
import { Component, QueryList, ViewChildren } from '@angular/core';
import { Router, RouterModule } from '@angular/router';
import { CommonModule } from '@angular/common';
import { MatBadgeModule } from '@angular/material/badge';
import { MatSlideToggleModule } from '@angular/material/slide-toggle';
import { FormsModule } from '@angular/forms';
import { MatTooltip, MatTooltipModule } from '@angular/material/tooltip';
import { MatButtonModule } from '@angular/material/button';
import { MatIconModule } from '@angular/material/icon';
import { AuthService } from '../../services/auth.service'
import { Observable } from 'rxjs';
@Component({
standalone: true,
imports: [RouterModule, CommonModule, MatIconModule, MatBadgeModule, MatSlideToggleModule, FormsModule, MatButtonModule, MatTooltipModule],
selector: 'app-navbar',
templateUrl: './navbar.component.html',
styleUrls: ['./navbar.component.scss']
})
export class NavbarComponent {
isLogedIn(): boolean {
return this.authService.IsAuthenticated;
}
async logInOut(): Promise<void> {
if (this.isLogedIn())
return this.authService.logoutAsync().then(() => {
this.router.navigate(['/login']);
})
else
this.router.navigate(['/login']);
}
isExpanded = false;
isChecked = true;
constructor(public router: Router, public authService: AuthService) {
}
get isDarkTheme(): boolean {
return false;
}
collapse() {
this.isExpanded = false;
}
toggle() {
this.isExpanded = !this.isExpanded;
}
@ViewChildren(MatTooltip) tooltips: QueryList<MatTooltip> | undefined;
}

View File

@@ -0,0 +1,16 @@
<form class="example-form">
<mat-form-field class="example-full-width">
<mat-label>Email</mat-label>
<input type="text" aria-label="Email" matInput [formControl]="control" [matAutocomplete]="auto">
@if (text) {
<button matSuffix mat-icon-button aria-label="Clear" (click)="text=''">
<mat-icon>close</mat-icon>
</button>
}
<mat-autocomplete autoActiveFirstOption #auto="matAutocomplete">
@for (option of filteredOptions | async; track option) {
<mat-option [value]="option">{{option}}</mat-option>
}
</mat-autocomplete>
</mat-form-field>
</form>

View File

@@ -0,0 +1,7 @@
.mat-stepper-vertical {
margin-top: 8px;
}
.mat-mdc-form-field {
margin-top: 16px;
}

View File

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

View File

@@ -0,0 +1,66 @@
import { Component, Input, OnChanges, OnInit, SimpleChanges } from '@angular/core';
import { ReactiveFormsModule, FormControl } from '@angular/forms';
import { MatInputModule } from '@angular/material/input';
import { Observable, map, startWith } from 'rxjs';
import { AsyncPipe } from '@angular/common';
import { MatAutocompleteModule } from '@angular/material/autocomplete';
import { MatIconModule } from '@angular/material/icon';
import { MatButtonModule } from '@angular/material/button';
import { FormsModule } from '@angular/forms';
import { MatFormFieldModule } from '@angular/material/form-field';
@Component({
selector: 'receiver-input',
standalone: true,
imports: [
MatInputModule,
MatAutocompleteModule,
ReactiveFormsModule,
AsyncPipe,
MatFormFieldModule, MatInputModule, FormsModule, MatButtonModule, MatIconModule
],
templateUrl: './receiver-input.component.html',
styleUrl: './receiver-input.component.scss'
})
export class ReceiverInputComponent implements OnInit, OnChanges {
ngOnInit(): void {
this.setupFiltering();
}
ngOnChanges(changes: SimpleChanges): void {
if (changes['options']) {
this.setupFiltering();
}
}
private setupFiltering(): void {
this.filteredOptions = this.control.valueChanges.pipe(
startWith(''),
map(value => this.filter(value || '', this.options, this.index)),
);
}
control = new FormControl('');
filteredOptions!: Observable<string[]>;
@Input() options: string[] = [];
@Input() filter: (value: string, options: string[], index?: number) => string[] = value => {
const filterValue = value.toLowerCase();
return this.options.filter(option => option.toLowerCase().includes(filterValue));
}
@Input() index?: number;
public get text(): string {
return this.control.value || '';
}
public set text(value: string) {
this.control.setValue(value)
}
public get lenght(): number {
return this.text.length;
}
}

View File

@@ -0,0 +1,23 @@
<table mat-table [dataSource]="receiverData" class="mat-elevation-z8">
<ng-container matColumnDef="email">
<th mat-header-cell *matHeaderCellDef> Email </th>
<td mat-cell *matCellDef="let element; let i = index">
<receiver-input [options]="receiver_mails" [filter]="receiver_filter"></receiver-input>
</td>
</ng-container>
<ng-container matColumnDef="name">
<th mat-header-cell *matHeaderCellDef> Anrede Email </th>
<td mat-cell *matCellDef="let element">
<clearable-input [label]="'Anrede Email'" [value]="element.name"></clearable-input>
</td>
</ng-container>
<ng-container matColumnDef="accessCode">
<th mat-header-cell *matHeaderCellDef> Zugriffscode </th>
<td mat-cell *matCellDef="let element"> {{element.accessCode}} </td>
</ng-container>
<tr mat-header-row *matHeaderRowDef="displayedColumns"></tr>
<tr mat-row *matRowDef="let row; columns: displayedColumns;"></tr>
</table>

View File

@@ -0,0 +1,59 @@
@if(isFilterable) {
<mat-form-field>
<mat-label>{{filter.label}}</mat-label>
<input matInput (keyup)="applyFilter($event)" [placeholder]="filter.placeholder" #input>
</mat-form-field>
}
<table mat-table [dataSource]="dataSource" multiTemplateDataRows class="mat-elevation-z8" matSort>
@for (column of __columnsToDisplay; track column) {
<ng-container matColumnDef="{{column}}">
@if(isSortable) {
<th mat-header-cell *matHeaderCellDef mat-sort-header> {{schema[column].header}} </th>
}
@else {
<th mat-header-cell *matHeaderCellDef> {{schema[column].header}} </th>
}
<td mat-cell *matCellDef="let element"> {{schema[column].field(element)}} </td>
</ng-container>
}
<!-- Expanded Content Column - The detail row is made up of this one column that spans across all columns -->
@if(isExpandable) {
<ng-container matColumnDef="expand">
<th mat-header-cell *matHeaderCellDef aria-label="row actions">&nbsp;</th>
<td mat-cell *matCellDef="let element">
<button mat-icon-button aria-label="expand row" (click)="toggleExpandedRow(element, $event)">
@if (__expandedElement === element) {
<mat-icon>keyboard_arrow_up</mat-icon>
} @else {
<mat-icon>keyboard_arrow_down</mat-icon>
}
</button>
</td>
</ng-container>
<ng-container matColumnDef="expandedDetail">
<td mat-cell *matCellDef="let element" [attr.colspan]="__columnsToDisplayWithExpand.length">
<div class="example-element-detail" [@detailExpand]="element == __expandedElement ? 'expanded' : 'collapsed'">
@if(__expandedElement === element){
<ng-content select="[expanded]" detailed></ng-content>
}
</div>
</td>
</ng-container>
}
@if(isExpandable) {
<tr mat-header-row *matHeaderRowDef="__columnsToDisplayWithExpand"></tr>
<tr mat-row *matRowDef="let element; columns: __columnsToDisplayWithExpand;" class="example-element-row"
[class.example-expanded-row]="__expandedElement === element" (click)="toggleExpandedRow(element, $event)">
</tr>
<tr mat-row *matRowDef="let row; columns: ['expandedDetail']" class="example-detail-row"></tr>
}
@else {
<tr mat-header-row *matHeaderRowDef="__columnsToDisplay"></tr>
<tr mat-row *matRowDef="let row; columns: __columnsToDisplay;"></tr>
}
</table>
@if(paginatorSizeOptions && paginatorSizeOptions.length > 0) {
<mat-paginator [pageSizeOptions]="paginatorSizeOptions" aria-label="Select page of users"></mat-paginator>
}

View File

@@ -0,0 +1,52 @@
table {
width: 100%;
}
tr.example-detail-row {
height: 0;
}
tr.example-element-row:not(.example-expanded-row):hover {
background: whitesmoke;
}
tr.example-element-row:not(.example-expanded-row):active {
background: #efefef;
}
.example-element-row td {
border-bottom-width: 0;
}
.example-element-detail {
overflow: hidden;
display: flex;
}
.example-element-diagram {
min-width: 80px;
border: 2px solid black;
padding: 8px;
font-weight: lighter;
margin: 8px 0;
height: 104px;
}
.example-element-symbol {
font-weight: bold;
font-size: 40px;
line-height: normal;
}
.example-element-description {
padding: 16px;
}
.example-element-description-attribution {
opacity: 0.5;
}
.mat-mdc-form-field {
font-size: 14px;
width: 100%;
}

View File

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

View File

@@ -0,0 +1,103 @@
import { AfterViewInit, Component, Input, ViewChild, inject } from '@angular/core';
import { animate, state, style, transition, trigger } from '@angular/animations';
import { MatIconModule } from '@angular/material/icon';
import { MatButtonModule } from '@angular/material/button';
import { MatTable, MatTableDataSource, MatTableModule } from '@angular/material/table';
import { ConfigurationService } from '../../../services/configuration.service';
import { MatInputModule } from '@angular/material/input';
import { MatFormFieldModule } from '@angular/material/form-field';
import { MatSort, MatSortModule } from '@angular/material/sort';
import { MatPaginator, MatPaginatorModule } from '@angular/material/paginator';
/**
* @title Table with expandable rows
*/
@Component({
selector: 'dd-table',
styleUrl: 'dd-table.component.scss',
templateUrl: 'dd-table.component.html',
animations: [
trigger('detailExpand', [
state('collapsed,void', style({ height: '0px', minHeight: '0' })),
state('expanded', style({ height: '*' })),
transition('expanded <=> collapsed', animate('225ms cubic-bezier(0.4, 0.0, 0.2, 1)')),
]),
],
standalone: true,
imports: [
MatTableModule,
MatButtonModule,
MatIconModule,
MatFormFieldModule,
MatInputModule,
MatTableModule,
MatSort,
MatSortModule,
MatPaginator,
MatPaginatorModule
],
})
export class DDTable implements AfterViewInit {
public readonly dataSource: any = new MatTableDataSource();
@Input() public set columnsToDisplay(value: string[]) {
this.__columnsToDisplay = value;
this.__columnsToDisplayWithExpand = [...value, 'expand'];
}
@Input() public set data(value: any[]) {
this.dataSource.data = value;
}
@Input() schema: Record<string, { header: string; field: (element: any) => any; }> = {}
@Input() paginatorSizeOptions?: number[];
@Input() filter: { label: string, placeholder: string } = { label: '', placeholder: '' }
@Input() isFilterable: boolean = false;
@Input() isExpandable: boolean = false;
@Input() isSortable: boolean = false;
@Input() onToggleExpandedRow: (element: any, event: Event) => Promise<void> = async (element: any, event: Event) => { }
public get data(): any[] {
return this.dataSource.data;
}
__columnsToDisplay: string[] = [];
__columnsToDisplayWithExpand: string[] = [];
__expandedElement!: any;
config: ConfigurationService = inject(ConfigurationService);
@ViewChild(MatSort) sort!: MatSort;
@ViewChild(MatPaginator) paginator!: MatPaginator;
@ViewChild(MatTable) table!: MatTable<any>;
ngAfterViewInit(): void {
if (this.isSortable)
this.dataSource.sort = this.sort;
if (this.paginatorSizeOptions && this.paginatorSizeOptions.length > 0)
this.dataSource.paginator = this.paginator;
}
applyFilter(event: Event) {
const filterValue = (event.target as HTMLInputElement).value;
this.dataSource.filter = filterValue.trim().toLowerCase();
}
update() {
this.table.renderRows();
}
async toggleExpandedRow(element: any, event: Event): Promise<void> {
// first determine the new expanded element, thus it would be possible to use up-to-date
const newExpandedElement = this.__expandedElement === element ? null : element;
// before update the expanded element call the call-back method to show up-to-date component
await this.onToggleExpandedRow(newExpandedElement, event);
// assign expanded element
this.__expandedElement = newExpandedElement;
event.stopPropagation();
}
}

View File

@@ -0,0 +1,12 @@
<dd-table [data]="data" [columnsToDisplay]="displayedColumns" [schema]="schema"
[paginatorSizeOptions]="[5, 10, 25, 100]" [filter]="{label: 'Filter', placeholder: ''}"
[onToggleExpandedRow]="onToggleExpandedRow" [isSortable]="true" [isExpandable]="true" [isFilterable]="true">
<mat-tab-group expanded>
<mat-tab label="Emfänger">
<receiver-status-table></receiver-status-table>
</mat-tab>
<mat-tab label="History">
<history-table></history-table>
</mat-tab>
</mat-tab-group>
</dd-table>

View File

@@ -0,0 +1,59 @@
/* Structure */
table {
width: 100%;
}
.mat-mdc-form-field {
font-size: 14px;
width: 100%;
margin-top: 10px;
}
/* For expanding table */
table {
width: 100%;
}
tr.example-detail-row {
height: 0;
}
tr.example-element-row:not(.example-expanded-row):hover {
background: whitesmoke;
}
tr.example-element-row:not(.example-expanded-row):active {
background: #efefef;
}
.example-element-row td {
border-bottom-width: 0;
}
.example-element-detail {
overflow: hidden;
display: flex;
}
.example-element-diagram {
min-width: 80px;
border: 2px solid black;
padding: 8px;
font-weight: lighter;
margin: 8px 0;
height: 104px;
}
.example-element-symbol {
font-weight: bold;
font-size: 40px;
line-height: normal;
}
.example-element-description {
padding: 16px;
}
.example-element-description-attribution {
opacity: 0.5;
}

View File

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

View File

@@ -0,0 +1,82 @@
import { AfterViewInit, Component, Input, ViewChild, inject, viewChild } from '@angular/core';
import { EnvelopeService } from '../../../services/envelope.service';
import { animate, state, style, transition, trigger } from '@angular/animations';
import { ConfigurationService } from '../../../services/configuration.service';
import { DDTable } from "../dd-table/dd-table.component";
import { ClearableInputComponent } from '../../clearable-input/clearable-input.component'
import { MatTabsModule } from '@angular/material/tabs';
import { ReceiverStatusTableComponent } from "../receiver-status-table/receiver-status-table.component";
import { HistoryTableComponent } from "../history-table/history-table.component";
import { EnvelopeReceiverService } from '../../../services/envelope-receiver.service';
import { HistoryService } from '../../../services/history.service';
@Component({
selector: 'envelope-table',
standalone: true,
imports: [DDTable, ClearableInputComponent, MatTabsModule, ReceiverStatusTableComponent, HistoryTableComponent],
templateUrl: './envelope-table.component.html',
animations: [
trigger('detailExpand', [
state('collapsed,void', style({ height: '0px', minHeight: '0' })),
state('expanded', style({ height: '*' })),
transition('expanded <=> collapsed', animate('225ms cubic-bezier(0.4, 0.0, 0.2, 1)')),
]),
],
styleUrl: './envelope-table.component.scss'
})
export class EnvelopeTableComponent implements AfterViewInit {
@Input() options?: { min_status?: number; max_status?: number; ignore_status?: number[] }
displayedColumns: string[] = ['title', 'status', 'type', 'addedWhen'];
schema: Record<string, { header: string; field: (element: any) => any; }> = {
'title': {
header: 'Title',
field: (element: any) => element.title
},
'status': {
header: 'Status',
field: (element: any) => element.statusName
},
'type': {
header: 'Type',
field: (element: any) => this.config.envelopeTypeTitles[element.contractType - 1]
},
'addedWhen': {
header: 'Added When',
field: (element: any) => element.addedWhen
}
}
data: any[] = [];
@ViewChild(ReceiverStatusTableComponent) rsTable!: ReceiverStatusTableComponent
@ViewChild(HistoryTableComponent) histTable!: HistoryTableComponent
onToggleExpandedRow: (envelope: any, event: Event) => Promise<void> = async (envelope, event) => {
if (envelope === null || envelope === undefined)
return;
var uuid: string = envelope.uuid;
this.rsTable.data = await this.erService.getSecretAsync(uuid);
var id: number = envelope.id;
var refType: number = this.config.referenceType.receiver;
const histories = await this.histService.getHistoryAsync({ envelopeId: id, referenceType: refType, withReceiver: true })
this.histTable.data = histories;
}
private eService: EnvelopeService = inject(EnvelopeService);
private config: ConfigurationService = inject(ConfigurationService);
private readonly erService: EnvelopeReceiverService = inject(EnvelopeReceiverService);
private readonly histService: HistoryService = inject(HistoryService);
async ngAfterViewInit() {
this.data = await this.eService.getEnvelopeAsync(this.options);
}
}

View File

@@ -0,0 +1 @@
<dd-table [data]="data" [columnsToDisplay]="columnsToDisplay" [schema]="schema" [isSortable]="true"></dd-table>

View File

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

View File

@@ -0,0 +1,30 @@
import { Component } from '@angular/core';
import { DDTable } from '../dd-table/dd-table.component'
@Component({
selector: 'history-table',
standalone: true,
imports: [DDTable],
templateUrl: './history-table.component.html',
styleUrl: './history-table.component.scss'
})
export class HistoryTableComponent {
data: any[] = [];
schema: Record<string, { header: string; field: (element: any) => any; }> = {
"status": {
"header": "Status",
"field": hist => hist.statusName
},
"user": {
"header": "Benutzer",
"field": hist => hist.userReference
},
"date": {
"header": "Datum",
"field": hist => hist.actionDate
}
}
columnsToDisplay: string[] = ["status", "user", "date"];
}

View File

@@ -0,0 +1 @@
<dd-table [data]="data" [columnsToDisplay]="columnsToDisplay" [schema]="schema" [isSortable]="true"></dd-table>

View File

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

View File

@@ -0,0 +1,30 @@
import { Component } from '@angular/core';
import { DDTable } from '../dd-table/dd-table.component'
@Component({
selector: 'receiver-status-table',
standalone: true,
imports: [DDTable],
templateUrl: './receiver-status-table.component.html',
styleUrl: './receiver-status-table.component.scss'
})
export class ReceiverStatusTableComponent {
data: any[] = [];
schema: Record<string, { header: string; field: (element: any) => any; }> = {
"name": {
"header": "Email Anrede",
"field": (er) => er.name
},
"email": {
"header": "Email",
"field": (er) => er.receiver.emailAddress
},
"access_code": {
"header": "Email",
"field": (er) => er.accessCode
},
}
columnsToDisplay: string[] = ["name", "email", "access_code"];
}

View File

@@ -0,0 +1,23 @@
<table mat-table [dataSource]="receiverData" class="mat-elevation-z8">
<ng-container matColumnDef="email">
<th mat-header-cell *matHeaderCellDef> Email </th>
<td mat-cell *matCellDef="let element; let i = index">
<receiver-input [options]="receiver_mails" [filter]="receiver_filter" [index]="i"></receiver-input>
</td>
</ng-container>
<ng-container matColumnDef="name">
<th mat-header-cell *matHeaderCellDef> Anrede Email </th>
<td mat-cell *matCellDef="let element">
<clearable-input [label]="'Anrede Email'" [value]="element.name"></clearable-input>
</td>
</ng-container>
<ng-container matColumnDef="accessCode">
<th mat-header-cell *matHeaderCellDef> Zugriffscode </th>
<td mat-cell *matCellDef="let element"> {{element.accessCode}} </td>
</ng-container>
<tr mat-header-row *matHeaderRowDef="displayedColumns"></tr>
<tr mat-row *matRowDef="let row; columns: displayedColumns;"></tr>
</table>

View File

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

View File

@@ -0,0 +1,100 @@
import { Component, OnInit, QueryList, ViewChildren } from '@angular/core';
import { FormsModule, ReactiveFormsModule } from '@angular/forms';
import { MatInputModule } from '@angular/material/input';
import { MatTableModule } from '@angular/material/table';
import { AsyncPipe } from '@angular/common';
import { MatAutocompleteModule } from '@angular/material/autocomplete';
import { MatFormFieldModule } from '@angular/material/form-field';
import { ReceiverService } from '../../../services/receiver.service'
import { ReceiverInputComponent } from '../../receiver-input/receiver-input.component';
import { MatIconModule } from '@angular/material/icon';
import { MatButtonModule } from '@angular/material/button';
import { ClearableInputComponent } from '../../clearable-input/clearable-input.component'
import { v4 as uuidv4 } from 'uuid';
@Component({
selector: 'receiver-table',
standalone: true,
imports: [
MatTableModule,
FormsModule,
MatFormFieldModule,
MatInputModule,
MatAutocompleteModule,
ReactiveFormsModule,
AsyncPipe,
ReceiverInputComponent,
MatFormFieldModule, MatInputModule, FormsModule, MatButtonModule, MatIconModule, ClearableInputComponent
],
templateUrl: './receiver-table.component.html',
styleUrl: './receiver-table.component.scss'
})
export class ReceiverTableComponent implements OnInit {
constructor(private receiverService: ReceiverService) { }
async ngOnInit() {
const receivers = await this.receiverService.getReceiverAsync();
this.receiver_mails = receivers.map((r: any) => r.emailAddress);
this.last_used_name = receivers.reduce((acc: any, r: any) => {
acc[r.emailAddress] = r.lastUsedName;
return acc;
}, {} as { [key: string]: string | null });
}
receiver_filter: (value: string, options: string[], index?: number) => string[] = (value, options, index) => {
const filterValue = value.toLowerCase();
// set the name if it is used before
const name = this.last_used_name[value];
if (name && index != null && index != undefined) {
this.receiverData.at(index)!.name = name
}
// !!! do not allow email duplication !!!
var similarMails = this.receiver_mails.filter(m => m.toLocaleLowerCase() === value.toLocaleLowerCase() && m !== value)
if (similarMails.length > 0 && index != undefined) {
this.receiverInputs[index].text = similarMails[0];
}
// if added into last row
if (value.length > 0 && (this.receiverInputs.at(-1)?.lenght ?? 0) > 0) {
this.receiverData.at(-1)!.accessCode = generateAccessCode();
this.receiverData.push({ email: "", name: "", accessCode: "" });
this.update();
}
// delete the row with out mail
else if (value.length === 0 && this.receiverInputs.length - 1 !== index) {
if (this.receiverInputs.length > 1 && this.receiverInputs[index!]?.lenght === 0) {
this.receiverData.splice(index!, 1);
this.update();
}
}
return options.filter(option => option.toLowerCase().includes(filterValue));
}
receiver_mails: string[] = [];
last_used_name: { [key: string]: string | null } = {};
@ViewChildren(ReceiverInputComponent) receiverInputsQueryList!: QueryList<ReceiverInputComponent>;
get receiverInputs(): ReceiverInputComponent[] {
return this.receiverInputsQueryList?.toArray() ?? [];
}
receiverData: { email: string; name: string; accessCode: string }[] = [
{ email: "", name: "", accessCode: "" }
];
displayedColumns: string[] = ['email', 'name', 'accessCode'];
public update() {
this.receiverData = [...this.receiverData];
}
}
function generateAccessCode(): string {
const uuid = uuidv4();
return uuid.replace(/-/g, '').substring(1, 7).toUpperCase();
}