This commit is contained in:
2024-10-05 00:15:13 +02:00
commit 0c5fb0e1c1
145 changed files with 23168 additions and 0 deletions
@@ -0,0 +1,56 @@
<div class="container">
<div class="flex column fill center middle">
<form action="{{apiUrl}}/login" method="POST" #loginForm class="box">
<mat-card>
<mat-card-content>
<img class="logo" src="assets/images/banner.png">
<h2>{{'login.internal' | i18n}}</h2>
<mat-error *ngIf="loginInvalid">
{{'login.invalid' | i18n}}
</mat-error>
<mat-form-field>
<mat-label>{{'login.username' | i18n}}</mat-label>
<input id="username" name="username" matInput required matAutofocus [value]="username">
<mat-error>
{{'login.username.missing' | i18n}}
</mat-error>
</mat-form-field>
<mat-form-field>
<mat-label>{{'login.password' | i18n}}</mat-label>
<input id="password" name="password" matInput type="password" required>
<mat-error>
{{'login.password.invalid.hint' | i18n}}
</mat-error>
</mat-form-field>
<mat-slide-toggle (change)="rememberMe.value = '' + $event.checked">
{{'login.keepSession' | i18n}}
</mat-slide-toggle>
<input #rememberMe id="remember-me" name="remember-me" type="hidden">
</mat-card-content>
<mat-card-actions>
<button type="submit" (click)="loginForm.submit()" mat-raised-button color="primary"
[disabled]="loginForm.invalid">{{'login' |
i18n}}<mat-icon style="font-size: 1em;">open_in_new
</mat-icon></button>
</mat-card-actions>
</mat-card>
</form>
<mat-card *ngIf="externals && externals.length > 0" class="box">
<mat-card-content>
<h2>{{'login.external' | i18n}}</h2>
<mat-error *ngIf="externalLoginInvalid">
{{'login.external.invalid' | i18n}}
</mat-error>
</mat-card-content>
<mat-card-actions class="flex wrap">
<a class="external-login" (click)="externalLogin(client)" *ngFor="let client of externals"
mat-raised-button color="accent">{{'login.external.client' | i18n:('login.provider.' + client.id |
i18n)}}</a>
<mat-slide-toggle [(ngModel)]="autologin">
{{'login.autologin' | i18n}}
</mat-slide-toggle>
</mat-card-actions>
</mat-card>
</div>
</div>
@@ -0,0 +1,33 @@
img.logo {
width: 300px;
height: auto;
}
mat-form-field,
mat-slide-toggle {
display: block;
margin-bottom: 25px;
}
a.external-login {
margin: 15px 0;
flex-basis: 100%;
flex-shrink: 0;
}
.box {
margin: 5px;
@media screen and (min-width: 576px) {
max-width: 100%;
}
@media screen and (min-width: 768px) {
max-width: 80%;
margin: 15px;
}
@media screen and (min-width: 992px) {
max-width: 50%;
}
}
@@ -0,0 +1,81 @@
import { Component, ElementRef, OnInit, ViewChild } from '@angular/core';
import { ActivatedRoute, Router } from '@angular/router';
import { environment } from '../../../environments/environment';
import { AuthService } from '../../services/auth.service';
@Component({
selector: 'page-login',
templateUrl: './login.page.html',
styleUrls: ['./login.page.scss']
})
export class PageLogin implements OnInit {
@ViewChild('loginForm') loginForm: ElementRef;
autologin: boolean = false;
loginInvalid: boolean;
externalLoginInvalid: boolean;
apiUrl = environment.apiUrl;
targetRoute: string;
externals: any[];
username: string = '';
constructor(
private authService: AuthService,
private router: Router,
private route: ActivatedRoute) { }
async ngOnInit() {
this.route.queryParams.subscribe({
next: (params) => {
if (params['target']) {
this.targetRoute = params['target'];
this.router.navigate([], { queryParams: { target: null }, queryParamsHandling: 'merge', replaceUrl: true });
}
if (params['error'] || params['error'] == '') {
this.loginInvalid = true;
this.router.navigate([], { queryParams: { error: null }, queryParamsHandling: 'merge', replaceUrl: true });
}
if (params['username']) {
this.username = params['username'];
this.router.navigate([], { queryParams: { username: null }, queryParamsHandling: 'merge', replaceUrl: true });
}
if (params['externalError'] || params['externalError'] == '') {
this.externalLoginInvalid = true;
this.router.navigate([], { queryParams: { externalError: null }, queryParamsHandling: 'merge', replaceUrl: true });
}
}
});
this.authService.getExternal().subscribe({
next: (data: any[]) => {
this.externals = data;
const autologinClient = localStorage.getItem("buntspecht.autologin");
for (let client of this.externals) {
if (client.id == autologinClient) {
window.location.href = this.apiUrl + "/" + client.loginUrl;
}
}
}
})
}
ngAfterViewInit(): void {
if (this.targetRoute) {
this.loginForm.nativeElement.action = this.loginForm.nativeElement.action + "?forward=" + window.location.origin + encodeURIComponent(this.targetRoute);
}
}
externalLogin(client: any): void {
if (this.autologin) {
localStorage.setItem("buntspecht.autologin", client.id);
} else {
localStorage.removeItem("buntspecht.autologin");
}
window.location.href = this.apiUrl + "/" + client.loginUrl;
}
}
@@ -0,0 +1,117 @@
<div class="flex column fill">
@if (entries && entries.error) {
<div class="flex column fill">
<mat-card class="accent box">
<mat-card-header>
<mat-card-title>{{ 'management.error.' + entries.error.status | i18n}}</mat-card-title>
<mat-card-subtitle>{{'management.error' | i18n}}</mat-card-subtitle>
</mat-card-header>
<mat-card-content>
<p>
{{ 'management.error.' + entries.error.status + '.text' | i18n}}
</p>
</mat-card-content>
</mat-card>
</div>
}
<div class="flex wrap filter-container">
<form class="flex wrap filter">
<mat-form-field class="margin">
<mat-label>{{'management.filter.created' | i18n}}</mat-label>
<mat-date-range-input [rangePicker]="picker">
<input matStartDate placeholder="{{'turnovers.filter.created.from' | i18n}}"
[value]="entries && entries.filter && entries.filter.from"
(dateChange)="setFilter('from', $event.value && $event.value.toISOString() || undefined)">
<input matEndDate placeholder="{{'turnovers.filter.created.to' | i18n}}"
[value]="entries && entries.filter && entries.filter.to"
(dateChange)="setFilter('to', $event.value && $event.value.endOf('day').toISOString() || undefined)">
</mat-date-range-input>
<mat-datepicker-toggle matIconSuffix [for]="picker"></mat-datepicker-toggle>
<mat-date-range-picker #picker></mat-date-range-picker>
</mat-form-field>
<mat-form-field class="margin">
<mat-label>{{'management.filter.username' | i18n}}</mat-label>
<input type="text" matInput [matAutocomplete]="auto" [formControl]="usersFormControl"
(change)="setInputFilter('username', $event.target)">
<mat-autocomplete #auto="matAutocomplete" (optionSelected)="setFilter('username', $event.option.value)">
@for (user of users | async; track user.username) {
<mat-option [value]="user.username">{{user.username}}</mat-option>
}
</mat-autocomplete>
</mat-form-field>
</form>
</div>
@if (entries) {
<div class="scroll-container">
<table class="default-table" mat-table [dataSource]="entries.results || []" matSort
(matSortChange)="applySort($event)" [matSortDisableClear]="true">
<ng-container matColumnDef="username">
<th mat-header-cell *matHeaderCellDef mat-sort-header [disableClear]="false">{{'user.username' |
i18n}}
</th>
<td mat-cell *matCellDef="let entry">
<div class="flex middle">
{{entry[0]}}
</div>
</td>
</ng-container>
<ng-container matColumnDef="price">
<th mat-header-cell *matHeaderCellDef mat-sort-header>
<span class="spacer"></span>
<span>{{'turnover.price' | i18n}}</span>
</th>
<td mat-cell *matCellDef="let entry">
<div class="flex">
<span class="spacer"></span>
<span>{{entry[1] | number: '1.2-2'}}</span>
<span>&nbsp;{{'turnover.price.suffix' | i18n}}</span>
</div>
</td>
</ng-container>
<ng-container matColumnDef="timeInvestment">
<th mat-header-cell *matHeaderCellDef mat-sort-header>
<span class="spacer"></span>
<span>{{'turnover.timeInvestment' | i18n}}</span>
</th>
<td mat-cell *matCellDef="let entry">
<div class="flex">
<span class="spacer"></span>
<span>{{entry[2] | number: '1.1-1'}}</span>
<span> &nbsp;{{'turnover.timeInvestment.suffix' | i18n}}</span>
</div>
</td>
</ng-container>
<tr mat-header-row *matHeaderRowDef="columns; sticky: true"></tr>
<tr class="entry" mat-row *matRowDef="let row; columns: columns;"></tr>
</table>
</div>
@if (entries.total == 0) {
<mat-list>
<mat-list-item>
<p>{{'paginator.empty' | i18n}}</p>
</mat-list-item>
</mat-list>
}
<span class="spacer"></span>
<div class="mat-mdc-paginator flex">
<span class="spacer"></span>
<mat-paginator [pageSizeOptions]="pageSizeOptions" [pageIndex]="entries.offset / entries.limit"
[length]="entries.total" [pageSize]="entries.limit" (page)="applyPage($event)" showFirstLastButtons>
</mat-paginator>
</div>
}
@if (!entries || !entries.results && !entries.error) {
<mat-progress-bar *ngIf="" mode="indeterminate"></mat-progress-bar>
}
</div>
@@ -0,0 +1,79 @@
import { Component, Input, OnInit } from '@angular/core';
import { FormControl } from '@angular/forms';
import { PageEvent } from '@angular/material/paginator';
import { Sort } from '@angular/material/sort';
import { debounceTime, Observable, switchMap } from 'rxjs';
import { TurnoverManagementService } from 'src/app/services/turnover.management.service';
import { UserManagementService } from 'src/app/services/user.management.service';
@Component({
selector: 'ui-management',
templateUrl: './management.page.html',
styleUrls: ['./management.page.scss']
})
export class PageManagement implements OnInit {
@Input() entries: any;
pageSizeOptions: number[] = [1, 2, 3, 4, 5, 10, 15, 30, 50, 100];
sort: string = "username";
descending: boolean = false;
columns: string[] = ['username', 'price', 'timeInvestment'];
users: Observable<any>;
usersFormControl = new FormControl();
constructor(
private turnoverManagementService: TurnoverManagementService,
private userManagementService: UserManagementService
) { }
ngOnInit(): void {
this.entries = {};
this.update();
this.users = this.usersFormControl
.valueChanges
.pipe(
debounceTime(300),
switchMap(value => this.userManagementService.pick(value))
);
}
update() {
const filter = JSON.parse(JSON.stringify(this.entries.filter || {}));
this.turnoverManagementService.overview(this.entries.limit || 15, this.entries.offset || 0, this.sort, this.descending, filter).subscribe({
next: (data: any) => {
this.entries = data;
this.entries.filter = filter;
}, error: (error) => {
this.entries = { error: error };
}
})
}
applyPage(event: PageEvent) {
this.entries.limit = event.pageSize;
this.entries.offset = event.pageSize * event.pageIndex;
this.update();
}
applySort(event: Sort) {
this.sort = event.direction ? event.active : 'username';
this.descending = event.direction !== 'asc';
this.update();
}
setInputFilter(key: string, target: EventTarget) {
this.setFilter(key, (target as HTMLInputElement).value);
}
setFilter(key: string, value) {
if (value != this.entries.filter[key]) {
this.entries.filter[key] = value;
this.entries.offset = 0;
this.update();
}
}
}
@@ -0,0 +1,15 @@
<div class="container">
<div class="flex column fill center middle">
<mat-card class="accent box">
<mat-card-header>
<mat-card-title>404</mat-card-title>
<mat-card-subtitle>{{'not-found' | i18n}}</mat-card-subtitle>
</mat-card-header>
<mat-card-content>
<p>
{{'not-found.text' | i18n}}
</p>
</mat-card-content>
</mat-card>
</div>
</div>
@@ -0,0 +1,17 @@
.box {
margin: 5px;
min-width: 400px;
@media screen and (min-width: 576px) {
max-width: 100%;
}
@media screen and (min-width: 768px) {
max-width: 80%;
margin: 15px;
}
@media screen and (min-width: 992px) {
max-width: 50%;
}
}
@@ -0,0 +1,12 @@
import { Component } from '@angular/core';
@Component({
selector: 'page-notfound',
templateUrl: './notfound.page.html',
styleUrls: [ './notfound.page.scss' ]
})
export class PageNotFound {
constructor() { }
}
@@ -0,0 +1,36 @@
<div class="flex column fill middle">
<form [formGroup]="passwordForm" (ngSubmit)="setPassword()">
<mat-card>
<mat-card-content>
<mat-card-title>{{'password' | i18n}}</mat-card-title>
<mat-form-field>
<mat-label>{{'password.old' | i18n}}</mat-label>
<input matInput formControlName="old" type="password">
<mat-error *ngFor="let error of passwordForm.get('old').errors | keyvalue">
{{'password.error.' + error.key | i18n}}
</mat-error>
</mat-form-field>
<mat-form-field>
<mat-label>{{'password.new' | i18n}}</mat-label>
<input matInput formControlName="password" type="password">
<mat-error *ngFor="let error of passwordForm.get('password').errors | keyvalue">
{{'password.error.' + error.key | i18n}}
</mat-error>
</mat-form-field>
<mat-form-field>
<mat-label>{{'password.repeat' | i18n}}</mat-label>
<input matInput formControlName="password2" type="password">
<mat-error *ngFor="let error of passwordForm.get('password2').errors | keyvalue">
{{'password.error.' + error.key | i18n}}
</mat-error>
</mat-form-field>
</mat-card-content>
<mat-card-actions>
<button type="submit" *ngIf="!working" mat-raised-button color="primary" [disabled]="passwordForm.invalid">
{{'password.update' | i18n}}
</button>
<a *ngIf="passwordSuccess" mat-button color="primary">{{'password.success' | i18n}}</a>
</mat-card-actions>
</mat-card>
</form>
</div>
@@ -0,0 +1,22 @@
mat-form-field {
display: block;
margin: 25px 0 !important;
}
form {
margin: 5px;
min-width: 400px;
@media screen and (min-width: 576px) {
max-width: 100%;
}
@media screen and (min-width: 768px) {
max-width: 80%;
margin: 15px;
}
@media screen and (min-width: 992px) {
max-width: 50%;
}
}
@@ -0,0 +1,69 @@
import { Component, OnDestroy, OnInit } from '@angular/core';
import { AbstractControlOptions, FormBuilder, FormGroup, Validators } from '@angular/forms';
import { UserService } from '../../services/user.service';
import { MatchingValidator } from 'src/app/utils/matching.validator';
@Component({
selector: 'page-password',
templateUrl: './password.page.html',
styleUrls: ['./password.page.scss']
})
export class PagePassword implements OnInit, OnDestroy {
auth: any;
working: boolean = false;
passwordSuccess: boolean = false;
passwordForm: FormGroup;
constructor(
private userService: UserService,
private formBuilder: FormBuilder) { }
ngOnInit(): void {
this.passwordForm = this.formBuilder.group({
old: ['', Validators.nullValidator],
password: ['', Validators.nullValidator],
password2: ['', Validators.nullValidator]
}, {
validator: MatchingValidator('password', 'password2')
} as AbstractControlOptions);
}
ngOnDestroy(): void {
}
passwordHasError(controlName: string): boolean {
return this.passwordForm.controls[controlName].errors != null;
}
setPassword() {
if (this.working) {
return;
}
this.working = true;
this.passwordSuccess = false;
this.userService.setPassword(this.passwordForm.get('old').value, this.passwordForm.get('password').value, this.passwordForm.get('password2').value).subscribe({
next: () => {
this.working = false;
this.passwordSuccess = true;
},
error: (error) => {
this.working = false;
if (error.status == 409) {
let errors = {};
for (let code of error.error) {
errors[code.field] = errors[code.field] || {};
errors[code.field][code.code] = true;
}
for (let code in errors) {
this.passwordForm.get(code).setErrors(errors[code]);
}
}
}
})
}
}
@@ -0,0 +1,79 @@
<div class="flex column fill middle">
<form [formGroup]="profileForm" (ngSubmit)="saveProfile()" *ngIf="user">
<mat-card>
<mat-card-content>
<mat-form-field>
<mat-label>{{'profile.username' | i18n}}</mat-label>
<input matInput formControlName="username" type="name">
</mat-form-field>
<mat-form-field>
<mat-label>{{'profile.name' | i18n}}</mat-label>
<input matInput formControlName="name" type="name">
<mat-error *ngIf="profileHasError('name')">
{{'profile.name.error' | i18n}}
</mat-error>
</mat-form-field>
<mat-form-field>
<mat-label>{{'profile.email' | i18n}}</mat-label>
<input matInput formControlName="email" type="email">
<mat-error *ngIf="profileHasError('email')">
{{'profile.email.error' | i18n}}
</mat-error>
</mat-form-field>
<mat-form-field>
<mat-label>{{'profile.about' | i18n}}</mat-label>
<textarea matAutosize matAutosizeMinRows="3" matInput formControlName="about"></textarea>
<mat-error>
{{'profile.about.error' | i18n}}
</mat-error>
</mat-form-field>
@if (admin) {
<mat-slide-toggle class="margin" [checked]="isAdmin" (change)="isAdmin=$event.checked">
{{'user.admin' | i18n}}
</mat-slide-toggle>
}
</mat-card-content>
<mat-card-actions>
<button type="submit" *ngIf="!working" mat-raised-button color="primary" [disabled]="profileForm.invalid">
{{'profile.update' | i18n}}
</button>
<a *ngIf="profileSuccess" mat-button color="primary">{{'profile.success' | i18n}}</a>
</mat-card-actions>
</mat-card>
</form>
@if(admin) {
<form [formGroup]="passwordForm" (ngSubmit)="setPassword()">
<mat-card>
<mat-card-content>
<mat-form-field>
<mat-label>{{'password.new' | i18n}}</mat-label>
<input matInput formControlName="password" type="password">
<mat-error *ngFor="let error of passwordForm.get('password').errors | keyvalue">
{{'password.error.' + error.key | i18n}}
</mat-error>
</mat-form-field>
<mat-form-field>
<mat-label>{{'password.repeat' | i18n}}</mat-label>
<input matInput formControlName="password2" type="password">
<mat-error *ngFor="let error of passwordForm.get('password2').errors | keyvalue">
{{'password.error.' + error.key | i18n}}
</mat-error>
</mat-form-field>
</mat-card-content>
<mat-card-actions>
<button type="submit" *ngIf="!working" mat-raised-button color="primary" [disabled]="passwordForm.invalid">
{{'password.update' | i18n}}
</button>
<a *ngIf="passwordSuccess" mat-button color="primary">{{'password.success' | i18n}}</a>
@if (admin && user && user.username) {
<span class="spacer"></span>
<a mat-raised-button color="warn" (click)="deleteUser()">
<mat-icon>delete</mat-icon> {{'user.delete' | i18n}}
</a>
}
</mat-card-actions>
</mat-card>
</form>
}
</div>
@@ -0,0 +1,23 @@
mat-form-field,
mat-slide-toggle {
display: block;
margin: 25px 0 !important;
}
form {
margin: 5px;
min-width: 400px;
@media screen and (min-width: 576px) {
max-width: 100%;
}
@media screen and (min-width: 768px) {
max-width: 80%;
margin: 15px;
}
@media screen and (min-width: 992px) {
max-width: 50%;
}
}
@@ -0,0 +1,176 @@
import { Component, OnDestroy, OnInit } from '@angular/core';
import { AbstractControlOptions, FormBuilder, FormGroup, Validators } from '@angular/forms';
import { UserService } from '../../services/user.service';
import { ActivatedRoute, Router } from '@angular/router';
import { UserManagementService } from 'src/app/services/user.management.service';
import { MatchingValidator } from 'src/app/utils/matching.validator';
import { ConfirmDialog } from 'src/app/ui/confirm/confirm.component';
import { MatDialog } from '@angular/material/dialog';
@Component({
selector: 'page-profile',
templateUrl: './profile.page.html',
styleUrls: ['./profile.page.scss']
})
export class PageProfile implements OnInit, OnDestroy {
auth: any;
user: any;
working: boolean = false;
profileSuccess: boolean = false;
profileForm: FormGroup;
passwordSuccess: boolean = false;
passwordForm: FormGroup;
admin: boolean = false;
isAdmin: boolean = false;
constructor(
private userService: UserService,
private userManagementService: UserManagementService,
private formBuilder: FormBuilder,
private router: Router,
private route: ActivatedRoute,
public dialog: MatDialog) { }
ngOnInit(): void {
this.profileForm = this.formBuilder.group({
username: [{ disabled: true }, Validators.nullValidator],
email: ['', Validators.nullValidator],
name: ['', Validators.nullValidator],
about: ['', Validators.nullValidator]
});
this.passwordForm = this.formBuilder.group({
password: ['', Validators.nullValidator],
password2: ['', Validators.nullValidator]
}, {
validator: MatchingValidator('password', 'password2')
} as AbstractControlOptions);
this.profileForm.get('username').disable();
let userFetch = this.userService.get();
if (this.route.snapshot.paramMap.has('username')) {
this.admin = true;
userFetch = this.userManagementService.get(this.route.snapshot.paramMap.get('username'));
}
userFetch.subscribe({
next: (user) => {
this.user = user;
this.isAdmin = this.user.roles && this.user.roles.indexOf('ROLE_ADMIN') != -1;
this.profileForm.get('username').setValue(this.user.username);
this.profileForm.get('name').setValue(this.user.name);
this.profileForm.get('email').setValue(this.user.email);
this.profileForm.get('about').setValue(this.user.about);
}
})
}
ngOnDestroy(): void {
}
profileHasError(controlName: string): boolean {
return this.profileForm.controls[controlName].errors != null;
}
saveProfile(): void {
if (this.working) {
return;
}
this.working = true;
this.profileSuccess = false;
this.user.about = this.profileForm.get('about').value;
this.user.email = this.profileForm.get('email').value;
this.user.name = this.profileForm.get('name').value;
if (this.isAdmin && (!this.user.roles || this.user.roles.indexOf('ROLE_ADMIN') == -1)) {
this.user.roles = this.user.roles || [];
this.user.roles.push('ROLE_ADMIN');
} else if (!this.isAdmin && this.user.roles && this.user.roles.indexOf('ROLE_ADMIN') != -1) {
this.user.roles.splice(this.user.roles.indexOf('ROLE_ADMIN'), 1);
}
const create = this.admin ? this.userManagementService.update(this.user) : this.userService.update(this.user);
create.subscribe({
next: (data) => {
this.user = data;
this.isAdmin = this.user.roles && this.user.roles.indexOf('ROLE_ADMIN') != -1;
this.working = false;
this.profileSuccess = true;
},
error: (error) => {
this.working = false;
if (error.status == 422) {
let errors = {};
for (let code of error.error) {
errors[code.field] = errors[code.field] || {};
errors[code.field][code.code] = true;
}
for (let code in errors) {
this.profileForm.get(code).setErrors(errors[code]);
}
}
}
})
}
setPassword() {
if (this.working) {
return;
}
this.working = true;
this.passwordSuccess = false;
this.userManagementService.setPassword(this.user.username, this.passwordForm.get('password').value).subscribe({
next: () => {
this.working = false;
this.passwordSuccess = true;
},
error: (error) => {
this.working = false;
if (error.status == 409) {
let errors = {};
for (let code of error.error) {
errors[code.field] = errors[code.field] || {};
errors[code.field][code.code] = true;
}
for (let code in errors) {
this.passwordForm.get(code).setErrors(errors[code]);
}
}
}
})
}
deleteUser() {
const dialogRef = this.dialog.open(ConfirmDialog, {
data: {
'label': 'user.confirmDelete',
'args': [this.user.username]
}
})
dialogRef.afterClosed().subscribe({
next: (result) => {
if (result) {
this.userManagementService.deleteUser(this.user.username).subscribe({
next: () => {
this.router.navigateByUrl('/');
}
});
}
}
});
}
}
@@ -0,0 +1,89 @@
<div class="flex column fill middle">
@if (!turnover) {
<mat-progress-bar mode="indeterminate"></mat-progress-bar>
}
@if (turnover) {
<form [formGroup]="form" (ngSubmit)="turnover.id ? update() : create()" #formDirective="ngForm">
<mat-card>
<mat-card-content>
<div class="flex space-between">
<p>{{ (turnover.id ? 'turnover.edit' : 'turnover.info') | i18n}}</p>
@if (turnover.created) {
<span>{{(turnover.username == username ? 'turnover.created.label' : 'turnover.created.label.username') |
i18n:(turnover.created | datef:'LLL' ):turnover.username}}</span>
}
</div>
<mat-form-field>
<mat-label>{{'turnover.customer' | i18n}}</mat-label>
<input matInput formControlName="customer" type="text" [required]="true">
<mat-error *ngIf="hasError('customer')">
{{'turnover.customer.error' | i18n}}
</mat-error>
</mat-form-field>
<mat-form-field>
<mat-label>{{'turnover.motif' | i18n}}</mat-label>
<input matInput formControlName="motif" type="text" [required]="true">
<mat-error *ngIf="hasError('motif')">
{{'turnover.motif.error' | i18n}}
</mat-error>
</mat-form-field>
<mat-form-field>
<mat-label>{{'turnover.price' | i18n}}</mat-label>
<input matInput formControlName="price" type="number" min="0" step="0.01" [required]="true">
<span matTextSuffix>{{'turnover.price.suffix' | i18n}}</span>
<mat-error *ngIf="hasError('price')">
{{'turnover.price.error' | i18n}}
</mat-error>
</mat-form-field>
<mat-form-field>
<mat-label>{{'turnover.timeInvestment' | i18n}}</mat-label>
<input matInput formControlName="timeInvestment" type="number" min="0" step="0.1">
<span matTextSuffix>{{'turnover.timeInvestment.suffix' | i18n}}</span>
<mat-error *ngIf="hasError('timeInvestment')">
{{'turnover.timeInvestment.error' | i18n}}
</mat-error>
</mat-form-field>
<mat-form-field>
<mat-label>{{'turnover.remark' | i18n}}</mat-label>
<textarea matAutosize matAutosizeMinRows="3" matInput formControlName="remark"></textarea>
<mat-error *ngIf="hasError('remark')">
{{'turnover.remark.error' | i18n}}
</mat-error>
</mat-form-field>
<mat-form-field>
<mat-label>{{'turnover.materialConsumption' | i18n}}</mat-label>
<textarea matAutosize matAutosizeMinRows="3" matInput formControlName="materialConsumption"></textarea>
<mat-error *ngIf="hasError('materialConsumption')">
{{'turnover.materialConsumption.error' | i18n}}
</mat-error>
</mat-form-field>
</mat-card-content>
<mat-card-actions>
@if (!working) {
<button type="submit" mat-raised-button color="primary" [disabled]="form.invalid">
{{(turnover.id ? 'turnover.update' : 'turnover.create') | i18n}}
</button>
}
@if (turnover.updated && turnover.updated != turnover.created) {
<span class="margin">{{'turnover.updated.label' | i18n:(turnover.updated | datef:'LLL' )}}</span>
}
@if (success) {
<a mat-button color="primary" disabled="true">{{'turnover.success' | i18n}}</a>
}
@if (admin && turnover.id) {
<span class="spacer"></span>
<a mat-raised-button color="warn" (click)="deleteTurnover()">
<mat-icon>delete</mat-icon> {{'turnover.delete' | i18n}}
</a>
}
</mat-card-actions>
</mat-card>
</form>
}
</div>
@@ -0,0 +1,27 @@
mat-form-field {
display: block;
margin: 25px 0 !important;
}
mat-chip mat-icon.mat-icon-inline {
margin-top: -12px;
margin-right: -2px;
}
form {
margin: 5px;
min-width: 400px;
@media screen and (min-width: 576px) {
max-width: 100%;
}
@media screen and (min-width: 768px) {
max-width: 80%;
margin: 15px;
}
@media screen and (min-width: 992px) {
max-width: 50%;
}
}
@@ -0,0 +1,196 @@
import { Component, OnInit } from '@angular/core';
import { FormBuilder, FormGroup, Validators } from '@angular/forms';
import { MatDialog } from '@angular/material/dialog';
import { MatSnackBar } from '@angular/material/snack-bar';
import { ActivatedRoute, Router } from '@angular/router';
import { AuthService } from 'src/app/services/auth.service';
import { TurnoverManagementService } from 'src/app/services/turnover.management.service';
import { TurnoverService } from 'src/app/services/turnover.service';
import { ConfirmDialog } from 'src/app/ui/confirm/confirm.component';
@Component({
selector: 'page-turnover',
templateUrl: './turnover.page.html',
styleUrls: ['./turnover.page.scss']
})
export class PageTurnover implements OnInit {
id: number;
turnover: any;
notfound: boolean = false;
working: boolean = false;
success: boolean = false;
form: FormGroup;
username: string = "";
admin: boolean = false;
constructor(
private turnoverService: TurnoverService,
private turnoverManagementService: TurnoverManagementService,
private authService: AuthService,
private formBuilder: FormBuilder,
private router: Router,
private route: ActivatedRoute,
private snackBar: MatSnackBar,
private dialog: MatDialog) { }
ngOnInit(): void {
this.form = this.formBuilder.group({
customer: ['', Validators.required],
motif: ['', Validators.required],
price: ['', Validators.required],
timeInvestment: ['', Validators.nullValidator],
remark: ['', Validators.nullValidator],
materialConsumption: ['', Validators.nullValidator],
});
this.id = this.route.snapshot.paramMap.get('id') && +this.route.snapshot.paramMap.get('id');
this.refresh();
}
refresh() {
if (this.id) {
let request = this.turnoverService.get(this.id);
this.authService.auth.subscribe({
next: (auth) => {
this.username = auth.username;
this.admin = auth.authorities && auth.authorities.find((role) => role.authority == 'ROLE_ADMIN') != undefined;
if (this.admin) {
request = this.turnoverManagementService.get(this.id);
}
request.subscribe({
next: (data) => {
this.turnover = data;
this.form.get("customer").setValue(this.turnover.customer);
this.form.get("motif").setValue(this.turnover.motif);
this.form.get("price").setValue(this.turnover.price);
this.form.get("timeInvestment").setValue(this.turnover.timeInvestment);
this.form.get("remark").setValue(this.turnover.remark);
this.form.get("materialConsumption").setValue(this.turnover.materialConsumption);
},
error: (error) => {
if (error.status == 404) {
this.notfound = true;
}
}
})
},
error: (error) => {
this.username = ""
}
})
} else {
this.turnover = {};
}
}
hasError(controlName: string): boolean {
return this.form.controls[controlName].errors != null;
}
create(): void {
if (this.working) {
return;
}
this.working = true;
this.turnover.customer = this.form.get("customer").value;
this.turnover.motif = this.form.get("motif").value;
this.turnover.price = this.form.get("price").value;
this.turnover.timeInvestment = this.form.get("timeInvestment").value;
this.turnover.remark = this.form.get("remark").value;
this.turnover.materialConsumption = this.form.get("materialConsumption").value;
this.turnoverService.create(this.turnover).subscribe({
next: (data) => {
this.router.navigateByUrl('/');
},
error: (error) => {
this.working = false;
if (error.status == 422) {
let errors = {};
for (let code of error.error) {
errors[code.field] = errors[code.field] || {};
errors[code.field][code.code] = true;
}
for (let code in errors) {
this.form.get(code).setErrors(errors[code]);
}
}
}
})
}
update(): void {
if (this.working) {
return;
}
this.working = true;
this.turnover.customer = this.form.get("customer").value;
this.turnover.motif = this.form.get("motif").value;
this.turnover.price = this.form.get("price").value;
this.turnover.timeInvestment = this.form.get("timeInvestment").value;
this.turnover.remark = this.form.get("remark").value;
this.turnover.materialConsumption = this.form.get("materialConsumption").value;
const request = this.admin ? this.turnoverManagementService.update(this.turnover) : this.turnoverService.update(this.turnover);
request.subscribe({
next: (data) => {
this.turnover = data;
this.working = false;
this.success = true;
},
error: (error) => {
this.working = false;
if (error.status == 403) {
this.snackBar.open("Error");
}
if (error.status == 422) {
let errors = {};
for (let code of error.error) {
errors[code.field] = errors[code.field] || {};
errors[code.field][code.code] = true;
}
for (let code in errors) {
this.form.get(code).setErrors(errors[code]);
}
}
}
})
}
deleteTurnover() {
const dialogRef = this.dialog.open(ConfirmDialog, {
data: {
'label': 'turnover.confirmDelete',
'args': [this.turnover.username]
}
})
dialogRef.afterClosed().subscribe({
next: (result) => {
if (result) {
this.turnoverManagementService.delete(this.turnover.id).subscribe({
next: () => {
this.router.navigateByUrl('/');
}
});
}
}
});
}
}
@@ -0,0 +1,39 @@
<div class="flex column fill">
<div class="flex wrap filter-container">
<a mat-icon-button (click)="filterOpen=!filterOpen" title="{{'turnovers.filter' | i18n}}"
[color]="filterOpen ? 'primary': 'accent'">
<mat-icon>filter_alt</mat-icon>
</a>
<form class="flex wrap filter" *ngIf="filterOpen">
<mat-form-field class="margin">
<mat-label>{{'turnovers.filter.created' | i18n}}</mat-label>
<mat-date-range-input [rangePicker]="picker">
<input matStartDate placeholder="{{'turnovers.filter.created.from' | i18n}}"
[value]="turnovers && turnovers.filter && turnovers.filter.from"
(dateChange)="setFilter('from', $event.value && $event.value.toISOString() || undefined)">
<input matEndDate placeholder="{{'turnovers.filter.created.to' | i18n}}"
[value]="turnovers && turnovers.filter && turnovers.filter.to"
(dateChange)="setFilter('to', $event.value && $event.value.endOf('day').toISOString() || undefined)">
</mat-date-range-input>
<mat-datepicker-toggle matIconSuffix [for]="picker"></mat-datepicker-toggle>
<mat-date-range-picker #picker></mat-date-range-picker>
</mat-form-field>
<mat-form-field class="margin">
<mat-label>{{'turnovers.filter.username' | i18n}}</mat-label>
<input type="text" matInput [matAutocomplete]="auto" [formControl]="usersFormControl"
(change)="setInputFilter('username', $event.target)">
<mat-autocomplete #auto="matAutocomplete" (optionSelected)="setFilter('username', $event.option.value)">
@for (user of users | async; track user.username) {
<mat-option [value]="user.username">{{user.username}}</mat-option>
}
</mat-autocomplete>
</mat-form-field>
</form>
</div>
<ui-turnovers class="flex column grow" [turnovers]="turnovers" (page)="applyPage($event)" (sort)="applySort($event)"
[username]="true"></ui-turnovers>
</div>
@@ -0,0 +1,22 @@
.filter-container {
padding-left: 15px;
justify-content: flex-start;
align-items: center;
.filter {
justify-content: flex-start;
align-items: center;
max-height: 70px;
&>* {
margin-top: 5px;
margin-bottom: 5px;
margin-left: 15px;
}
}
}
ui-turnovers {
min-height: 0;
}
@@ -0,0 +1,76 @@
import { Component, OnInit } from '@angular/core';
import { FormControl } from '@angular/forms';
import { PageEvent } from '@angular/material/paginator';
import { Sort } from '@angular/material/sort';
import { debounceTime, Observable, switchMap } from 'rxjs';
import { TurnoverManagementService } from 'src/app/services/turnover.management.service';
import { UserManagementService } from 'src/app/services/user.management.service';
@Component({
selector: 'page-turnovers-manage',
templateUrl: './manage.page.html',
styleUrls: ['./manage.page.scss']
})
export class PageTurnoversManage implements OnInit {
turnovers: any;
sort: string = "created";
descending: boolean = true;
filterOpen: boolean = false;
users: Observable<any>;
usersFormControl = new FormControl();
constructor(
private turnoverManagementService: TurnoverManagementService,
private userManagementService: UserManagementService
) { }
ngOnInit(): void {
this.turnovers = {};
this.update();
this.users = this.usersFormControl
.valueChanges
.pipe(
debounceTime(300),
switchMap(value => this.userManagementService.pick(value))
);
}
update() {
const filter = JSON.parse(JSON.stringify(this.turnovers.filter || {}));
this.turnoverManagementService.fetch(this.turnovers.limit || 15, this.turnovers.offset || 0, this.sort, this.descending, this.turnovers.filter).subscribe({
next: (data: any) => {
this.turnovers = data;
this.turnovers.filter = filter;
}, error: (error) => {
this.turnovers = { error: error };
}
})
}
applyPage(event: PageEvent) {
this.turnovers.limit = event.pageSize;
this.turnovers.offset = event.pageSize * event.pageIndex;
this.update();
}
applySort(event: Sort) {
this.sort = event.direction ? event.active : 'created';
this.descending = event.direction !== 'asc';
this.update();
}
setInputFilter(key: string, target: EventTarget) {
this.setFilter(key, (target as HTMLInputElement).value);
}
setFilter(key: string, value) {
if (value != this.turnovers.filter[key]) {
this.turnovers.filter[key] = value;
this.turnovers.offset = 0;
this.update();
}
}
}
@@ -0,0 +1,27 @@
<div class="flex column fill">
<div class="flex wrap filter-container">
<a mat-icon-button (click)="filterOpen=!filterOpen" title="{{'turnovers.filter' | i18n}}"
[color]="filterOpen ? 'primary': 'accent'">
<mat-icon>filter_alt</mat-icon>
</a>
<form class="flex wrap filter" *ngIf="filterOpen">
<mat-form-field class="margin">
<mat-label>{{'turnovers.filter.created' | i18n}}</mat-label>
<mat-date-range-input [rangePicker]="picker">
<input matStartDate placeholder="{{'turnovers.filter.created.from' | i18n}}"
[value]="turnovers && turnovers.filter && turnovers.filter.from"
(dateChange)="setFilter('from', $event.value && $event.value.toISOString() || undefined)">
<input matEndDate placeholder="{{'turnovers.filter.created.to' | i18n}}"
[value]="turnovers && turnovers.filter && turnovers.filter.to"
(dateChange)="setFilter('to', $event.value && $event.value.endOf('day').toISOString() || undefined)">
</mat-date-range-input>
<mat-datepicker-toggle matIconSuffix [for]="picker"></mat-datepicker-toggle>
<mat-date-range-picker #picker></mat-date-range-picker>
</mat-form-field>
</form>
</div>
<ui-turnovers class="flex column grow" [turnovers]="turnovers" (page)="applyPage($event)"
(sort)="applySort($event)"></ui-turnovers>
</div>
@@ -0,0 +1,23 @@
.filter-container {
padding-left: 15px;
justify-content: flex-start;
align-items: center;
.filter {
justify-content: flex-start;
align-items: center;
max-height: 70px;
&>* {
margin-top: 5px;
margin-bottom: 5px;
margin-left: 15px;
}
}
}
ui-turnovers {
min-height: 0;
}
@@ -0,0 +1,64 @@
import { Component, OnInit } from '@angular/core';
import { FormControl } from '@angular/forms';
import { PageEvent } from '@angular/material/paginator';
import { Sort } from '@angular/material/sort';
import { debounceTime, Observable, switchMap } from 'rxjs';
import { TurnoverService } from 'src/app/services/turnover.service';
import { UserManagementService } from 'src/app/services/user.management.service';
@Component({
selector: 'page-turnovers',
templateUrl: './turnovers.page.html',
styleUrls: ['./turnovers.page.scss']
})
export class PageTurnovers implements OnInit {
turnovers: any;
sort: string = "created";
descending: boolean = true;
filterOpen: boolean = false;
users: Observable<any>;
usersFormControl = new FormControl();
constructor(
private turnoverService: TurnoverService
) { }
ngOnInit(): void {
this.turnovers = {};
this.update();
}
update() {
const filter = JSON.parse(JSON.stringify(this.turnovers.filter || {}));
this.turnoverService.fetch(this.turnovers.limit || 15, this.turnovers.offset || 0, this.sort, this.descending, this.turnovers.filter).subscribe({
next: (data: any) => {
this.turnovers = data;
this.turnovers.filter = filter;
}, error: (error) => {
this.turnovers = { error: error };
}
})
}
applyPage(event: PageEvent) {
this.turnovers.limit = event.pageSize;
this.turnovers.offset = event.pageSize * event.pageIndex;
this.update();
}
applySort(event: Sort) {
this.sort = event.direction ? event.active : 'created';
this.descending = event.direction !== 'asc';
this.update();
}
setFilter(key: string, value) {
if (value != this.turnovers.filter[key]) {
this.turnovers.filter[key] = value;
this.turnovers.offset = 0;
this.update();
}
}
}
@@ -0,0 +1,20 @@
<div class="container">
<div class="flex column fill center middle">
<mat-card class="warn box">
<mat-card-header>
<mat-card-title>503</mat-card-title>
<mat-card-subtitle>{{'service-unavailable' | i18n}}</mat-card-subtitle>
</mat-card-header>
<mat-card-content>
<p>
{{'service-unavailable.text' | i18n}}
</p>
</mat-card-content>
<mat-card-actions>
<a mat-raised-button color="primary" (click)="retry()">
{{'service-unavailable.retry' | i18n}}
</a>
</mat-card-actions>
</mat-card>
</div>
</div>
@@ -0,0 +1,17 @@
.box {
margin: 5px;
min-width: 400px;
@media screen and (min-width: 576px) {
max-width: 100%;
}
@media screen and (min-width: 768px) {
max-width: 80%;
margin: 15px;
}
@media screen and (min-width: 992px) {
max-width: 50%;
}
}
@@ -0,0 +1,39 @@
import { Component, OnInit } from '@angular/core';
import { Location } from '@angular/common'
import { Router, ActivatedRoute } from '@angular/router';
@Component({
selector: 'page-unavailable',
templateUrl: './unavailable.page.html',
styleUrls: ['./unavailable.page.scss']
})
export class PageUnavailable implements OnInit {
targetRoute = '';
constructor(
private location: Location,
private router: Router,
private route: ActivatedRoute) { }
ngOnInit(): void {
this.route.queryParams.subscribe({
next: (params) => {
if (params['target']) {
this.targetRoute = params['target'];
this.router.navigate([], { queryParams: { target: null }, queryParamsHandling: 'merge', skipLocationChange: true });
}
}
});
}
retry() {
if (!this.targetRoute || this.targetRoute === "unavailable" || this.targetRoute === "/unavailable") {
this.location.back;
} else {
this.router.navigate([this.targetRoute]);
}
}
}
@@ -0,0 +1,110 @@
@if (users && users.error) {
<div class="flex column fill">
<mat-card class="accent box">
<mat-card-header>
<mat-card-title>{{ 'users.error.' + users.error.status | i18n}}</mat-card-title>
<mat-card-subtitle>{{'users.error' | i18n}}</mat-card-subtitle>
</mat-card-header>
<mat-card-content>
<p>
{{ 'users.error.' + users.error.status + '.text' | i18n}}
</p>
</mat-card-content>
</mat-card>
</div>
}
@if (users) {
<div class="flex column fill">
<div class="scroll-container">
<table class="default-table" mat-table [dataSource]="users.results || []" matSort
(matSortChange)="applySort($event)" [matSortDisableClear]="true">
<ng-container matColumnDef="username">
<th mat-header-cell *matHeaderCellDef mat-sort-header [disableClear]="false">{{'user.username' |
i18n}}
</th>
<td mat-cell *matCellDef="let user">
<div class="flex middle">
@if (user.roles && user.roles.indexOf('ROLE_ADMIN') != -1) {
<mat-icon>admin_panel_settings</mat-icon>
}
{{user.username}}
</div>
</td>
</ng-container>
<ng-container matColumnDef="name">
<th mat-header-cell *matHeaderCellDef mat-sort-header>{{'profile.name' | i18n}}</th>
<td mat-cell *matCellDef="let user">{{user.name}}</td>
</ng-container>
<ng-container matColumnDef="email">
<th mat-header-cell *matHeaderCellDef>{{'profile.email' | i18n}}</th>
<td mat-cell *matCellDef="let user">{{user.email}}</td>
</ng-container>
<ng-container matColumnDef="about">
<th mat-header-cell *matHeaderCellDef>{{'profile.about' | i18n}}</th>
<td mat-cell *matCellDef="let user">
<span class="ellipsis" matTooltip="{{user.about}}">{{user.about}}</span>
</td>
</ng-container>
<tr mat-header-row *matHeaderRowDef="columns; sticky: true"></tr>
<tr class="user" mat-row *matRowDef="let user; columns: columns;" [routerLink]="'/u/' + user.username"></tr>
</table>
</div>
@if (users.total == 0) {
<mat-list>
<mat-list-item>
<p>{{'paginator.empty' | i18n}}</p>
</mat-list-item>
</mat-list>
}
<span class="spacer"></span>
<form [formGroup]="form" (ngSubmit)="createUser()" #formDirective="ngForm">
<div class="flex middle">
<mat-form-field class="margin">
<mat-label>{{'profile.username' | i18n}}</mat-label>
<input matInput formControlName="username" type="text" [required]="true">
</mat-form-field>
<mat-form-field class="margin">
<mat-label>{{'profile.name' | i18n}}</mat-label>
<input matInput formControlName="name" type="text">
</mat-form-field>
<mat-form-field class="margin">
<mat-label>{{'profile.email' | i18n}}</mat-label>
<input matInput formControlName="email" type="email">
</mat-form-field>
<mat-form-field class="margin">
<mat-label>{{'user.password' | i18n}}</mat-label>
<input matInput formControlName="password" type="password">
</mat-form-field>
<mat-slide-toggle class="margin" (change)="isAdmin=$event.checked">
{{'user.admin' | i18n}}
</mat-slide-toggle>
<button type="submit" mat-raised-button color="primary" [disabled]="form.invalid">{{'user.create' |
i18n}}<mat-icon style="font-size: 1em;">person_add</mat-icon></button>
</div>
</form>
<div class="mat-mdc-paginator flex">
<span class="spacer"></span>
<mat-paginator [pageSizeOptions]="pageSizeOptions" [pageIndex]="users.offset / users.limit"
[length]="users.total" [pageSize]="users.limit" (page)="applyPage($event)" showFirstLastButtons>
</mat-paginator>
</div>
</div>
}
@if (!users || !users.results && !users.error) {
<mat-progress-bar *ngIf="" mode="indeterminate"></mat-progress-bar>
}
@@ -0,0 +1,10 @@
tr.user {
&:hover {
cursor: pointer;
opacity: 0.7;
}
&.disabled {
pointer-events: none;
}
}
@@ -0,0 +1,134 @@
import { Component, HostListener, Input, OnInit } from '@angular/core';
import { FormBuilder, FormGroup, Validators } from '@angular/forms';
import { MatDialog } from '@angular/material/dialog';
import { PageEvent } from '@angular/material/paginator';
import { Sort } from '@angular/material/sort';
import { AuthService } from 'src/app/services/auth.service';
import { UserManagementService } from 'src/app/services/user.management.service';
import { ConfirmDialog } from 'src/app/ui/confirm/confirm.component';
@Component({
selector: 'ui-users',
templateUrl: './users.page.html',
styleUrls: ['./users.page.scss']
})
export class PageUsers implements OnInit {
@Input() users: any;
pageSizeOptions: number[] = [1, 2, 3, 4, 5, 10, 15, 30, 50, 100];
sort: string = "username";
descending: boolean = false;
columns: string[] = [];
form: FormGroup;
isAdmin: boolean = false;
username: string = "";
constructor(
private userManagementService: UserManagementService,
private authService: AuthService,
private formBuilder: FormBuilder) { }
ngOnInit(): void {
this.users = {};
this.update();
this.applyResize(window.innerWidth || document.documentElement.clientWidth || document.body.clientWidth);
this.form = this.formBuilder.group({
username: ['', Validators.required],
name: ['', Validators.nullValidator],
email: ['', Validators.nullValidator],
password: ['', Validators.required]
});
this.authService.auth.subscribe({
next: (auth) => {
this.username = auth.username;
},
error: (error) => {
this.username = ""
}
})
}
@HostListener('window:resize', ['$event'])
onResize(event) {
this.applyResize(event.target.innerWidth || event.target.documentElement.clientWidth || event.target.body.clientWidth)
}
applyResize(width: number) {
if (width < 992) {
this.columns = ['username', 'name', 'email']
} else {
this.columns = ['username', 'name', 'email', 'about'];
}
}
update() {
this.userManagementService.fetch(this.users.limit || 15, this.users.offset || 0, this.sort, this.descending).subscribe({
next: (data: any) => {
this.users = data;
}, error: (error) => {
this.users = { error: error };
}
})
}
applyPage(event: PageEvent) {
this.users.limit = event.pageSize;
this.users.offset = event.pageSize * event.pageIndex;
this.update();
}
applySort(event: Sort) {
this.sort = event.direction ? event.active : 'username';
this.descending = event.direction !== 'asc';
this.update();
}
createUser() {
const user = {
username: this.form.get("username").value,
name: this.form.get("name").value,
email: this.form.get("email").value,
roles: this.isAdmin ? ['ROLE_ADMIN'] : []
}
this.userManagementService.create(user).subscribe({
next: (result: any) => {
this.userManagementService.setPassword(result.username, this.form.get("password").value).subscribe({
next: (result) => {
this.update();
},
error: (error) => {
if (error.status == 422) {
let errors = {};
for (let code of error.error) {
errors[code.field] = errors[code.field] || {};
errors[code.field][code.code] = true;
}
for (let code in errors) {
this.form.get(code).setErrors(errors[code]);
}
}
}
});
},
error: (error) => {
if (error.status == 422) {
let errors = {};
for (let code of error.error) {
errors[code.field] = errors[code.field] || {};
errors[code.field][code.code] = true;
}
for (let code in errors) {
this.form.get(code).setErrors(errors[code]);
}
}
}
})
}
}