init
This commit is contained in:
@@ -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> {{'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> {{'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]);
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
}
|
||||
Reference in New Issue
Block a user