improve filter, add url parameter, overview improvements

This commit is contained in:
2024-10-05 15:14:46 +02:00
parent 4dda02c231
commit 0913d090b4
23 changed files with 630 additions and 172 deletions
@@ -12,6 +12,7 @@ import { PageTurnover } from './pages/turnover/turnover.page';
import { PageTurnoversManage } from './pages/turnovers/manage/manage.page'; import { PageTurnoversManage } from './pages/turnovers/manage/manage.page';
import { PageTurnovers } from './pages/turnovers/turnovers.page'; import { PageTurnovers } from './pages/turnovers/turnovers.page';
import { PageUnavailable } from './pages/unavailable/unavailable.page'; import { PageUnavailable } from './pages/unavailable/unavailable.page';
import { PageUserCreate } from './pages/users/create/users.create.page';
import { PageUsers } from './pages/users/users.page'; import { PageUsers } from './pages/users/users.page';
import { UiMain } from './ui/main/main.ui'; import { UiMain } from './ui/main/main.ui';
@@ -28,6 +29,7 @@ const routes: Routes = [
{ path: 't', component: PageTurnoversManage, canActivate: [AuthAdminGuard] }, { path: 't', component: PageTurnoversManage, canActivate: [AuthAdminGuard] },
{ path: 'm', component: PageManagement, canActivate: [AuthAdminGuard] }, { path: 'm', component: PageManagement, canActivate: [AuthAdminGuard] },
{ path: 'u', component: PageUsers, canActivate: [AuthAdminGuard] }, { path: 'u', component: PageUsers, canActivate: [AuthAdminGuard] },
{ path: 'user', component: PageUserCreate, canActivate: [AuthAdminGuard] },
{ path: 'u/:username', component: PageProfile, canActivate: [AuthAdminGuard] }, { path: 'u/:username', component: PageProfile, canActivate: [AuthAdminGuard] },
{ path: 'unavailable', component: PageUnavailable }, { path: 'unavailable', component: PageUnavailable },
{ path: '**', component: PageNotFound, pathMatch: 'full', canActivate: [AuthenticatedGuard] } { path: '**', component: PageNotFound, pathMatch: 'full', canActivate: [AuthenticatedGuard] }
+5 -3
View File
@@ -22,18 +22,19 @@ import { UiMain } from './ui/main/main.ui';
import { I18nEmptyPipe, I18nPipe } from './utils/i18n.pipe'; import { I18nEmptyPipe, I18nPipe } from './utils/i18n.pipe';
import { MomentPipe } from './utils/moment.pipe'; import { MomentPipe } from './utils/moment.pipe';
import { MAT_FORM_FIELD_DEFAULT_OPTIONS } from '@angular/material/form-field';
import { ServiceWorkerModule } from '@angular/service-worker'; import { ServiceWorkerModule } from '@angular/service-worker';
import { environment } from '../environments/environment'; import { environment } from '../environments/environment';
import { PageManagement } from './pages/management/management.page';
import { PagePassword } from './pages/password/password.page'; import { PagePassword } from './pages/password/password.page';
import { PageTurnover } from './pages/turnover/turnover.page'; import { PageTurnover } from './pages/turnover/turnover.page';
import { PageTurnoversManage } from './pages/turnovers/manage/manage.page'; import { PageTurnoversManage } from './pages/turnovers/manage/manage.page';
import { PageTurnovers } from './pages/turnovers/turnovers.page'; import { PageTurnovers } from './pages/turnovers/turnovers.page';
import { PageUserCreate } from './pages/users/create/users.create.page';
import { PageUsers } from './pages/users/users.page'; import { PageUsers } from './pages/users/users.page';
import { I18nPaginatorIntl, I18nService } from './services/i18n.service'; import { I18nPaginatorIntl, I18nService } from './services/i18n.service';
import { UiTurnovers } from './ui/turnovers/turnovers.ui';
import { MAT_FORM_FIELD_DEFAULT_OPTIONS } from '@angular/material/form-field';
import { ConfirmDialog } from './ui/confirm/confirm.component'; import { ConfirmDialog } from './ui/confirm/confirm.component';
import { PageManagement } from './pages/management/management.page'; import { UiTurnovers } from './ui/turnovers/turnovers.ui';
export function fetchI18n(i18n: I18nService) { export function fetchI18n(i18n: I18nService) {
@@ -80,6 +81,7 @@ export class XhrInterceptor implements HttpInterceptor {
PageProfile, PageProfile,
PageUnavailable, PageUnavailable,
PageUsers, PageUsers,
PageUserCreate,
UiMain, UiMain,
UiTurnovers, UiTurnovers,
ConfirmDialog ConfirmDialog
@@ -16,6 +16,12 @@
} }
<div class="flex wrap filter-container"> <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>
@if (filterOpen) {
<form class="flex wrap filter"> <form class="flex wrap filter">
<mat-form-field class="margin"> <mat-form-field class="margin">
<mat-label>{{'management.filter.created' | i18n}}</mat-label> <mat-label>{{'management.filter.created' | i18n}}</mat-label>
@@ -43,19 +49,30 @@
</mat-autocomplete> </mat-autocomplete>
</mat-form-field> </mat-form-field>
</form> </form>
}
</div> </div>
@if (entries) { @if (entries && entries.total == 0) {
<div class="scroll-container"> <mat-list>
<table class="default-table" mat-table [dataSource]="entries.results || []" matSort <mat-list-item>
<p>{{'paginator.empty' | i18n}}</p>
</mat-list-item>
</mat-list>
}
@if (entries && entries.total) {
<div class="flex scroll-container grow">
<table class="default-table grow" mat-table [dataSource]="entries.results || []" multiTemplateDataRows matSort
(matSortChange)="applySort($event)" [matSortDisableClear]="true"> (matSortChange)="applySort($event)" [matSortDisableClear]="true">
<ng-container matColumnDef="username"> <ng-container matColumnDef="username">
<th mat-header-cell *matHeaderCellDef mat-sort-header [disableClear]="false">{{'user.username' | <th mat-header-cell *matHeaderCellDef mat-sort-header [disableClear]="false">
i18n}} {{'user.username' | i18n}}
</th> </th>
<td mat-cell *matCellDef="let entry"> <td mat-cell *matCellDef="let entry">
<div class="flex middle"> <div class="flex middle">
{{entry[0]}} <a class="select-user"
[ngClass]="{'selected': entries.filter && entries.filter.username == entry[0]}"
(click)="selectUser(entry[0])">{{entry[0]}}</a>
</div> </div>
</td> </td>
</ng-container> </ng-container>
@@ -88,21 +105,40 @@
</td> </td>
</ng-container> </ng-container>
<ng-container matColumnDef="menu">
<th mat-header-cell *matHeaderCellDef></th>
<td mat-cell *matCellDef="let entry">
@if (entries.filter && entries.filter.username == entry[0]) {
<button mat-icon-button (click)="expanded = !expanded">
@if (expanded) {
<mat-icon>keyboard_arrow_up</mat-icon>
} @else {
<mat-icon>keyboard_arrow_down</mat-icon>
}
</button>
}
</td>
</ng-container>
<ng-container matColumnDef="expanded">
<td mat-cell *matCellDef="let entry" [attr.colspan]="columns.length">
@if (expanded && entries.total && entries.filter && entries.filter.username == entry[0]) {
<ui-turnovers class="flex column fill" [turnovers]="turnovers" (page)="applyTurnoverPage($event)"
[enableSort]="false"></ui-turnovers>
}
</td>
</ng-container>
<tr mat-header-row *matHeaderRowDef="columns; sticky: true"></tr> <tr mat-header-row *matHeaderRowDef="columns; sticky: true"></tr>
<tr class="entry" mat-row *matRowDef="let row; columns: columns;"></tr> <tr class="entry" mat-row *matRowDef="let row; columns: columns;"></tr>
<tr class="expanded-row" [ngClass]="{'visible' : expanded}" mat-row
*matRowDef="let row; columns: ['expanded']"></tr>
</table> </table>
</div> </div>
@if (entries.total == 0) {
<mat-list>
<mat-list-item>
<p>{{'paginator.empty' | i18n}}</p>
</mat-list-item>
</mat-list>
}
@if (!entries.filter || !entries.filter.username) {
<span class="spacer"></span> <span class="spacer"></span>
<div class="mat-mdc-paginator flex"> <div class="mat-mdc-paginator flex">
<span class="spacer"></span> <span class="spacer"></span>
<mat-paginator [pageSizeOptions]="pageSizeOptions" [pageIndex]="entries.offset / entries.limit" <mat-paginator [pageSizeOptions]="pageSizeOptions" [pageIndex]="entries.offset / entries.limit"
@@ -110,6 +146,7 @@
</mat-paginator> </mat-paginator>
</div> </div>
} }
}
@if (!entries || !entries.results && !entries.error) { @if (!entries || !entries.results && !entries.error) {
<mat-progress-bar *ngIf="" mode="indeterminate"></mat-progress-bar> <mat-progress-bar *ngIf="" mode="indeterminate"></mat-progress-bar>
@@ -0,0 +1,44 @@
.filter-container {
padding-left: 15px;
justify-content: flex-start;
align-items: center;
.filter {
justify-content: flex-start;
align-items: center;
&>* {
margin-top: 5px;
margin-bottom: 5px;
margin-left: 15px;
}
}
}
.expanded-row {
visibility: hidden;
height: 0;
transition: height 500ms ease-in-out;
&.visible {
visibility: visible;
height: auto;
}
td {
padding: 0;
vertical-align: top;
}
}
a.select-user {
cursor: pointer;
&.selected {
cursor: not-allowed;
}
&:hover {
opacity: 0.7;
}
}
@@ -2,6 +2,7 @@ import { Component, Input, OnInit } from '@angular/core';
import { FormControl } from '@angular/forms'; import { FormControl } from '@angular/forms';
import { PageEvent } from '@angular/material/paginator'; import { PageEvent } from '@angular/material/paginator';
import { Sort } from '@angular/material/sort'; import { Sort } from '@angular/material/sort';
import { ActivatedRoute, Params, Router } from '@angular/router';
import { debounceTime, Observable, switchMap } from 'rxjs'; import { debounceTime, Observable, switchMap } from 'rxjs';
import { TurnoverManagementService } from 'src/app/services/turnover.management.service'; import { TurnoverManagementService } from 'src/app/services/turnover.management.service';
import { UserManagementService } from 'src/app/services/user.management.service'; import { UserManagementService } from 'src/app/services/user.management.service';
@@ -17,41 +18,122 @@ export class PageManagement implements OnInit {
pageSizeOptions: number[] = [1, 2, 3, 4, 5, 10, 15, 30, 50, 100]; pageSizeOptions: number[] = [1, 2, 3, 4, 5, 10, 15, 30, 50, 100];
sort: string = "username"; sort: string = "username";
descending: boolean = false; descending: boolean = false;
filterOpen: boolean = true;
columns: string[] = ['username', 'price', 'timeInvestment']; columns: string[] = ['username', 'price', 'timeInvestment', 'menu'];
expanded: boolean = false;
users: Observable<any>; users: Observable<any>;
usersFormControl = new FormControl(); usersFormControl = new FormControl();
turnovers: any;
constructor( constructor(
private turnoverManagementService: TurnoverManagementService, private turnoverManagementService: TurnoverManagementService,
private userManagementService: UserManagementService private userManagementService: UserManagementService,
private router: Router,
private route: ActivatedRoute
) { } ) { }
ngOnInit(): void { ngOnInit(): void {
this.entries = {}; this.entries = {};
this.update(); this.turnovers = {};
this.users = this.usersFormControl this.users = this.usersFormControl
.valueChanges .valueChanges
.pipe( .pipe(
debounceTime(300), debounceTime(300),
switchMap(value => this.userManagementService.pick(value)) switchMap(value => this.userManagementService.pick(value))
); );
this.route.queryParams.subscribe({
next: (params) => {
this.entries = { filter: {} };
this.expanded = false;
if (params['l']) {
this.entries.limit = +params['l'];
if (this.entries.limit < 1) {
this.entries.limit = 1;
}
}
if (params['o']) {
this.entries.offset = +params['o'];
if (this.entries.offset < 0) {
this.entries.offset = 0;
}
}
if (params['s']) {
this.sort = params['s'];
}
if (params['a']) {
this.descending = false;
} else {
this.descending = true;
}
for (const param in params) {
if (param != 'l' && param != 'o' && param != 's' && param != 'a') {
this.entries.filter[param] = params[param];
}
}
this.refresh();
}
});
} }
update() { refresh() {
const filter = JSON.parse(JSON.stringify(this.entries.filter || {})); 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({ this.turnoverManagementService.overview(this.entries.limit || 15, this.entries.offset || 0, this.sort, this.descending, filter).subscribe({
next: (data: any) => { next: (data: any) => {
this.entries = data; this.entries = data;
this.entries.filter = filter; this.entries.filter = filter;
if (filter.username) {
this.applyUser(filter.username);
}
}, error: (error) => { }, error: (error) => {
this.entries = { error: error }; this.entries = { error: error };
} }
}) })
} }
update() {
const filter = JSON.parse(JSON.stringify(this.entries.filter || {}));
const params: Params = { l: null, o: null, s: null, a: null };
if ((this.entries.limit || 15) != 15) {
params['l'] = this.entries.limit;
}
if ((this.entries.offset || 0) != 0) {
params['o'] = this.entries.offset;
}
if (this.sort != 'username') {
params['s'] = this.sort;
}
if (!this.descending) {
params['a'] = true;
}
if (filter) {
for (const param in filter) {
params[param] = filter[param] || null;
}
}
this.router.navigate(
[],
{
relativeTo: this.route,
queryParams: params,
queryParamsHandling: 'replace'
});
}
applyPage(event: PageEvent) { applyPage(event: PageEvent) {
this.entries.limit = event.pageSize; this.entries.limit = event.pageSize;
this.entries.offset = event.pageSize * event.pageIndex; this.entries.offset = event.pageSize * event.pageIndex;
@@ -76,4 +158,36 @@ export class PageManagement implements OnInit {
} }
} }
selectUser(username: string) {
this.router.navigate(
[],
{
relativeTo: this.route,
queryParams: { username: this.entries.filter && this.entries.filter.username == username ? null : username },
queryParamsHandling: 'merge'
});
}
applyUser(username: string) {
this.usersFormControl.setValue(username);
if (this.entries.total) {
this.expanded = true;
const filter = JSON.parse(JSON.stringify(this.entries.filter || {}));
this.turnoverManagementService.fetch(this.turnovers.limit || 100, this.turnovers.offset || 0, 'created', true, filter).subscribe({
next: (data: any) => {
this.turnovers = data;
this.turnovers.filter = filter;
}, error: (error) => {
this.turnovers = { error: error };
}
})
}
}
applyTurnoverPage(event: PageEvent) {
this.turnovers.limit = event.pageSize;
this.turnovers.offset = event.pageSize * event.pageIndex;
this.applyUser(this.usersFormControl.value);
}
} }
@@ -166,7 +166,7 @@ export class PageProfile implements OnInit, OnDestroy {
if (result) { if (result) {
this.userManagementService.deleteUser(this.user.username).subscribe({ this.userManagementService.deleteUser(this.user.username).subscribe({
next: () => { next: () => {
this.router.navigateByUrl('/'); this.router.navigateByUrl('/u');
} }
}); });
} }
@@ -2,12 +2,6 @@ mat-form-field {
display: block; display: block;
margin: 20px 0 !important; margin: 20px 0 !important;
} }
mat-chip mat-icon.mat-icon-inline {
margin-top: -12px;
margin-right: -2px;
}
form { form {
margin: 5px; margin: 5px;
min-width: 390px; min-width: 390px;
@@ -5,7 +5,8 @@
<mat-icon>filter_alt</mat-icon> <mat-icon>filter_alt</mat-icon>
</a> </a>
<form class="flex wrap filter" *ngIf="filterOpen"> @if(filterOpen) {
<form class="flex wrap filter">
<mat-form-field class="margin"> <mat-form-field class="margin">
<mat-label>{{'turnovers.filter.created' | i18n}}</mat-label> <mat-label>{{'turnovers.filter.created' | i18n}}</mat-label>
<mat-date-range-input [rangePicker]="picker"> <mat-date-range-input [rangePicker]="picker">
@@ -24,6 +25,7 @@
<mat-form-field class="margin"> <mat-form-field class="margin">
<mat-label>{{'turnovers.filter.username' | i18n}}</mat-label> <mat-label>{{'turnovers.filter.username' | i18n}}</mat-label>
<input type="text" matInput [matAutocomplete]="auto" [formControl]="usersFormControl" <input type="text" matInput [matAutocomplete]="auto" [formControl]="usersFormControl"
[value]="turnovers && turnovers.filter && turnovers.filter.username || ''"
(change)="setInputFilter('username', $event.target)"> (change)="setInputFilter('username', $event.target)">
<mat-autocomplete #auto="matAutocomplete" (optionSelected)="setFilter('username', $event.option.value)"> <mat-autocomplete #auto="matAutocomplete" (optionSelected)="setFilter('username', $event.option.value)">
@for (user of users | async; track user.username) { @for (user of users | async; track user.username) {
@@ -34,14 +36,17 @@
<mat-form-field class="margin"> <mat-form-field class="margin">
<mat-label>{{'turnovers.filter.customer' | i18n}}</mat-label> <mat-label>{{'turnovers.filter.customer' | i18n}}</mat-label>
<input type="text" matInput (input)="setInputFilter('customer', $event.target)"> <input type="text" matInput [value]="turnovers && turnovers.filter && turnovers.filter.customer || ''"
(input)="setInputFilter('customer', $event.target)">
</mat-form-field> </mat-form-field>
<mat-form-field class="margin"> <mat-form-field class="margin">
<mat-label>{{'turnovers.filter.motif' | i18n}}</mat-label> <mat-label>{{'turnovers.filter.motif' | i18n}}</mat-label>
<input type="text" matInput (input)="setInputFilter('motif', $event.target)"> <input type="text" matInput [value]="turnovers && turnovers.filter && turnovers.filter.motif || ''"
(input)="setInputFilter('motif', $event.target)">
</mat-form-field> </mat-form-field>
</form> </form>
}
</div> </div>
<ui-turnovers class="flex column grow" [turnovers]="turnovers" (page)="applyPage($event)" (sort)="applySort($event)" <ui-turnovers class="flex column grow" [turnovers]="turnovers" (page)="applyPage($event)" (sort)="applySort($event)"
@@ -4,10 +4,8 @@
align-items: center; align-items: center;
.filter { .filter {
justify-content: flex-start; justify-content: flex-start;
align-items: center; align-items: center;
max-height: 70px;
&>* { &>* {
margin-top: 5px; margin-top: 5px;
@@ -2,6 +2,7 @@ import { Component, OnInit } from '@angular/core';
import { FormControl } from '@angular/forms'; import { FormControl } from '@angular/forms';
import { PageEvent } from '@angular/material/paginator'; import { PageEvent } from '@angular/material/paginator';
import { Sort } from '@angular/material/sort'; import { Sort } from '@angular/material/sort';
import { ActivatedRoute, Params, Router } from '@angular/router';
import { debounceTime, Observable, switchMap } from 'rxjs'; import { debounceTime, Observable, switchMap } from 'rxjs';
import { TurnoverManagementService } from 'src/app/services/turnover.management.service'; import { TurnoverManagementService } from 'src/app/services/turnover.management.service';
import { UserManagementService } from 'src/app/services/user.management.service'; import { UserManagementService } from 'src/app/services/user.management.service';
@@ -23,24 +24,64 @@ export class PageTurnoversManage implements OnInit {
constructor( constructor(
private turnoverManagementService: TurnoverManagementService, private turnoverManagementService: TurnoverManagementService,
private userManagementService: UserManagementService private userManagementService: UserManagementService,
private router: Router,
private route: ActivatedRoute
) { } ) { }
ngOnInit(): void { ngOnInit(): void {
this.turnovers = {}; this.turnovers = {};
this.update();
this.users = this.usersFormControl this.users = this.usersFormControl
.valueChanges .valueChanges
.pipe( .pipe(
debounceTime(300), debounceTime(300),
switchMap(value => this.userManagementService.pick(value)) switchMap(value => this.userManagementService.pick(value))
); );
this.route.queryParams.subscribe({
next: (params) => {
this.turnovers = { filter: {} };
if (params['l']) {
this.turnovers.limit = +params['l'];
if (this.turnovers.limit < 1) {
this.turnovers.limit = 1;
}
}
if (params['o']) {
this.turnovers.offset = +params['o'];
if (this.turnovers.offset < 0) {
this.turnovers.offset = 0;
}
}
if (params['s']) {
this.sort = params['s'];
}
if (params['a']) {
this.descending = false;
} else {
this.descending = true;
}
for (const param in params) {
if (param != 'l' && param != 'o' && param != 's' && param != 'a') {
this.filterOpen = true;
this.turnovers.filter[param] = params[param];
if (param == 'username') {
this.usersFormControl.setValue(params[param]);
}
}
}
this.refresh();
}
});
} }
update() { refresh() {
const filter = JSON.parse(JSON.stringify(this.turnovers.filter || {})); 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({ this.turnoverManagementService.fetch(this.turnovers.limit || 15, this.turnovers.offset || 0, this.sort, this.descending, filter).subscribe({
next: (data: any) => { next: (data: any) => {
this.turnovers = data; this.turnovers = data;
this.turnovers.filter = filter; this.turnovers.filter = filter;
@@ -50,6 +91,42 @@ export class PageTurnoversManage implements OnInit {
}) })
} }
update() {
const filter = JSON.parse(JSON.stringify(this.turnovers.filter || {}));
const params: Params = { l: null, o: null, s: null, a: null };
if ((this.turnovers.limit || 15) != 15) {
params['l'] = this.turnovers.limit;
}
if ((this.turnovers.offset || 0) != 0) {
params['o'] = this.turnovers.offset;
}
if (this.sort != 'created') {
params['s'] = this.sort;
}
if (!this.descending) {
params['a'] = true;
}
if (filter) {
for (const param in filter) {
params[param] = filter[param] || null;
}
}
this.router.navigate(
[],
{
relativeTo: this.route,
queryParams: params,
queryParamsHandling: 'replace'
});
}
applyPage(event: PageEvent) { applyPage(event: PageEvent) {
this.turnovers.limit = event.pageSize; this.turnovers.limit = event.pageSize;
this.turnovers.offset = event.pageSize * event.pageIndex; this.turnovers.offset = event.pageSize * event.pageIndex;
@@ -5,7 +5,8 @@
<mat-icon>filter_alt</mat-icon> <mat-icon>filter_alt</mat-icon>
</a> </a>
<form class="flex wrap filter" *ngIf="filterOpen"> @if (filterOpen) {
<form class="flex wrap filter">
<mat-form-field class="margin"> <mat-form-field class="margin">
<mat-label>{{'turnovers.filter.created' | i18n}}</mat-label> <mat-label>{{'turnovers.filter.created' | i18n}}</mat-label>
<mat-date-range-input [rangePicker]="picker"> <mat-date-range-input [rangePicker]="picker">
@@ -22,14 +23,17 @@
<mat-form-field class="margin"> <mat-form-field class="margin">
<mat-label>{{'turnovers.filter.customer' | i18n}}</mat-label> <mat-label>{{'turnovers.filter.customer' | i18n}}</mat-label>
<input type="text" matInput (input)="setInputFilter('customer', $event.target)"> <input type="text" matInput [value]="turnovers && turnovers.filter && turnovers.filter.customer || ''"
(input)="setInputFilter('customer', $event.target)">
</mat-form-field> </mat-form-field>
<mat-form-field class="margin"> <mat-form-field class="margin">
<mat-label>{{'turnovers.filter.motif' | i18n}}</mat-label> <mat-label>{{'turnovers.filter.motif' | i18n}}</mat-label>
<input type="text" matInput (input)="setInputFilter('motif', $event.target)"> <input type="text" matInput [value]="turnovers && turnovers.filter && turnovers.filter.motif || ''"
(input)="setInputFilter('motif', $event.target)">
</mat-form-field> </mat-form-field>
</form> </form>
}
</div> </div>
<ui-turnovers class="flex column grow" [turnovers]="turnovers" [overview]="overview" (page)="applyPage($event)" <ui-turnovers class="flex column grow" [turnovers]="turnovers" [overview]="overview" (page)="applyPage($event)"
@@ -7,7 +7,6 @@
justify-content: flex-start; justify-content: flex-start;
align-items: center; align-items: center;
max-height: 70px;
&>* { &>* {
margin-top: 5px; margin-top: 5px;
@@ -2,6 +2,7 @@ import { Component, OnInit } from '@angular/core';
import { FormControl } from '@angular/forms'; import { FormControl } from '@angular/forms';
import { PageEvent } from '@angular/material/paginator'; import { PageEvent } from '@angular/material/paginator';
import { Sort } from '@angular/material/sort'; import { Sort } from '@angular/material/sort';
import { ActivatedRoute, Params, Router } from '@angular/router';
import { debounceTime, Observable, switchMap } from 'rxjs'; import { debounceTime, Observable, switchMap } from 'rxjs';
import { TurnoverService } from 'src/app/services/turnover.service'; import { TurnoverService } from 'src/app/services/turnover.service';
import { UserManagementService } from 'src/app/services/user.management.service'; import { UserManagementService } from 'src/app/services/user.management.service';
@@ -18,20 +19,54 @@ export class PageTurnovers implements OnInit {
sort: string = "created"; sort: string = "created";
descending: boolean = true; descending: boolean = true;
filterOpen: boolean = false; filterOpen: boolean = false;
init: boolean = true;
users: Observable<any>;
usersFormControl = new FormControl();
constructor( constructor(
private turnoverService: TurnoverService private turnoverService: TurnoverService,
private router: Router,
private route: ActivatedRoute
) { } ) { }
ngOnInit(): void { ngOnInit(): void {
this.turnovers = {}; this.turnovers = {};
this.update(); this.route.queryParams.subscribe({
next: (params) => {
this.turnovers = { filter: {} };
if (params['l']) {
this.turnovers.limit = +params['l'];
if (this.turnovers.limit < 1) {
this.turnovers.limit = 1;
}
}
if (params['o']) {
this.turnovers.offset = +params['o'];
if (this.turnovers.offset < 0) {
this.turnovers.offset = 0;
}
}
if (params['s']) {
this.sort = params['s'];
}
if (params['a']) {
this.descending = false;
} else {
this.descending = true;
}
for (const param in params) {
if (param != 'l' && param != 'o' && param != 's' && param != 'a') {
this.filterOpen = true;
this.turnovers.filter[param] = params[param];
}
}
this.refresh();
}
});
} }
update() { refresh() {
const filter = JSON.parse(JSON.stringify(this.turnovers.filter || {})); 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({ this.turnoverService.fetch(this.turnovers.limit || 15, this.turnovers.offset || 0, this.sort, this.descending, this.turnovers.filter).subscribe({
next: (data: any) => { next: (data: any) => {
@@ -54,6 +89,42 @@ export class PageTurnovers implements OnInit {
}) })
} }
update() {
const filter = JSON.parse(JSON.stringify(this.turnovers.filter || {}));
const params: Params = { l: null, o: null, s: null, a: null };
if ((this.turnovers.limit || 15) != 15) {
params['l'] = this.turnovers.limit;
}
if ((this.turnovers.offset || 0) != 0) {
params['o'] = this.turnovers.offset;
}
if (this.sort != 'created') {
params['s'] = this.sort;
}
if (!this.descending) {
params['a'] = true;
}
if (filter) {
for (const param in filter) {
params[param] = filter[param] || null;
}
}
this.router.navigate(
[],
{
relativeTo: this.route,
queryParams: params,
queryParamsHandling: 'replace'
});
}
applyPage(event: PageEvent) { applyPage(event: PageEvent) {
this.turnovers.limit = event.pageSize; this.turnovers.limit = event.pageSize;
this.turnovers.offset = event.pageSize * event.pageIndex; this.turnovers.offset = event.pageSize * event.pageIndex;
@@ -0,0 +1,49 @@
<div class="flex column fill middle">
<form [formGroup]="form" (ngSubmit)="createUser()" #formDirective="ngForm">
<mat-card>
<mat-card-content>
<mat-form-field class="margin">
<mat-label>{{'profile.username' | i18n}}</mat-label>
<input matInput formControlName="username" type="text" [required]="true">
<mat-error *ngFor="let error of form.get('username').errors | keyvalue">
{{'user.error.' + error.key | i18n}}
</mat-error>
</mat-form-field>
<mat-form-field class="margin">
<mat-label>{{'profile.name' | i18n}}</mat-label>
<input matInput formControlName="name" type="text">
<mat-error *ngFor="let error of form.get('name').errors | keyvalue">
{{'user.error.' + error.key | i18n}}
</mat-error>
</mat-form-field>
<mat-form-field class="margin">
<mat-label>{{'profile.email' | i18n}}</mat-label>
<input matInput formControlName="email" type="email">
<mat-error *ngFor="let error of form.get('email').errors | keyvalue">
{{'user.error.' + error.key | i18n}}
</mat-error>
</mat-form-field>
<mat-form-field class="margin">
<mat-label>{{'user.password' | i18n}}</mat-label>
<input matInput formControlName="password" type="password">
<mat-error *ngFor="let error of form.get('password').errors | keyvalue">
{{'password.error.' + error.key | i18n}}
</mat-error>
</mat-form-field>
<mat-slide-toggle class="margin" (change)="isAdmin=$event.checked">
{{'user.admin' | i18n}}
</mat-slide-toggle>
</mat-card-content>
<mat-card-actions>
@if (!working) {
<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>
}
</mat-card-actions>
</mat-card>
</form>
</div>
@@ -0,0 +1,21 @@
mat-form-field {
display: block;
margin: 20px 0 !important;
}
form {
margin: 5px;
min-width: 390px;
@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,84 @@
import { Component, OnInit } from '@angular/core';
import { FormBuilder, FormGroup, Validators } from '@angular/forms';
import { Router } from '@angular/router';
import { UserManagementService } from 'src/app/services/user.management.service';
@Component({
selector: 'page-user-create',
templateUrl: './users.create.page.html',
styleUrls: ['./users.create.page.scss']
})
export class PageUserCreate implements OnInit {
form: FormGroup;
isAdmin: boolean = false;
working: boolean = false;
constructor(
private userManagementService: UserManagementService,
private formBuilder: FormBuilder,
private router: Router) { }
ngOnInit(): void {
this.form = this.formBuilder.group({
username: ['', Validators.required],
name: ['', Validators.nullValidator],
email: ['', Validators.nullValidator],
password: ['', Validators.required]
});
}
createUser() {
this.working = true;
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'] : []
}
const request = this.form.get("username").disabled ? this.userManagementService.update(user) : this.userManagementService.create(user);
request.subscribe({
next: (result: any) => {
this.userManagementService.setPassword(result.username, this.form.get("password").value).subscribe({
next: (result) => {
this.working = false;
this.router.navigateByUrl('/u');
},
error: (error) => {
this.form.get("username").disable();
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.form.get(code).setErrors(errors[code]);
}
}
}
});
},
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.form.get(code).setErrors(errors[code]);
}
}
}
})
}
}
@@ -1,21 +1,38 @@
@if (users && users.error) {
<div class="flex column fill"> <div class="flex column fill">
<mat-card class="accent box"> @if (users && users.error) {
<mat-card-header> <div class="flex column fill">
<mat-card-title>{{ 'users.error.' + users.error.status | i18n}}</mat-card-title> <mat-card class="accent box">
<mat-card-subtitle>{{'users.error' | i18n}}</mat-card-subtitle> <mat-card-header>
</mat-card-header> <mat-card-title>{{ 'users.error.' + users.error.status | i18n}}</mat-card-title>
<mat-card-content> <mat-card-subtitle>{{'users.error' | i18n}}</mat-card-subtitle>
<p> </mat-card-header>
{{ 'users.error.' + users.error.status + '.text' | i18n}} <mat-card-content>
</p> <p>
</mat-card-content> {{ 'users.error.' + users.error.status + '.text' | i18n}}
</mat-card> </p>
</div> </mat-card-content>
} </mat-card>
</div>
}
@if (users) { <div class="flex wrap filter-container">
<div class="flex column fill"> <a mat-icon-button (click)="filterOpen=!filterOpen" title="{{'users.filter' | i18n}}"
[color]="filterOpen ? 'primary': 'accent'">
<mat-icon>filter_alt</mat-icon>
</a>
@if (filterOpen) {
<form class="flex wrap filter">
<mat-form-field class="margin">
<mat-label>{{'users.filter.search' | i18n}}</mat-label>
<input type="text" matInput [value]="users && users.filter && users.filter || ''"
(input)="setFilter( $event.target)">
</mat-form-field>
</form>
}
</div>
@if (users) {
<div class="scroll-container"> <div class="scroll-container">
<table class="default-table" mat-table [dataSource]="users.results || []" matSort <table class="default-table" mat-table [dataSource]="users.results || []" matSort
(matSortChange)="applySort($event)" [matSortDisableClear]="true"> (matSortChange)="applySort($event)" [matSortDisableClear]="true">
@@ -51,7 +68,8 @@
</ng-container> </ng-container>
<tr mat-header-row *matHeaderRowDef="columns; sticky: true"></tr> <tr mat-header-row *matHeaderRowDef="columns; sticky: true"></tr>
<tr class="user" mat-row *matRowDef="let user; columns: columns;" [routerLink]="'/u/' + user.username"></tr> <tr class="user" mat-row *matRowDef="let user; columns: columns;" [routerLink]="'/u/' + user.username">
</tr>
</table> </table>
</div> </div>
@if (users.total == 0) { @if (users.total == 0) {
@@ -64,47 +82,17 @@
<span class="spacer"></span> <span class="spacer"></span>
<div class="mat-mdc-paginator flex middle">
<form [formGroup]="form" (ngSubmit)="createUser()" #formDirective="ngForm"> <a class="margin" routerLink="/user" mat-raised-button color="primary">{{'user.create' | i18n}}<mat-icon
<div class="flex wrap middle"> style="font-size: 1em;">person_add</mat-icon></a>
<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> <span class="spacer"></span>
<mat-paginator [pageSizeOptions]="pageSizeOptions" [pageIndex]="users.offset / users.limit" <mat-paginator [pageSizeOptions]="pageSizeOptions" [pageIndex]="users.offset / users.limit"
[length]="users.total" [pageSize]="users.limit" (page)="applyPage($event)" showFirstLastButtons> [length]="users.total" [pageSize]="users.limit" (page)="applyPage($event)" showFirstLastButtons>
</mat-paginator> </mat-paginator>
</div> </div>
</div> }
}
@if (!users || !users.results && !users.error) { @if (!users || !users.results && !users.error) {
<mat-progress-bar *ngIf="" mode="indeterminate"></mat-progress-bar> <mat-progress-bar *ngIf="" mode="indeterminate"></mat-progress-bar>
} }
</div>
@@ -1,11 +1,9 @@
import { Component, HostListener, Input, OnInit } from '@angular/core'; import { Component, HostListener, Input, OnInit } from '@angular/core';
import { FormBuilder, FormGroup, Validators } from '@angular/forms'; import { FormBuilder } from '@angular/forms';
import { MatDialog } from '@angular/material/dialog';
import { PageEvent } from '@angular/material/paginator'; import { PageEvent } from '@angular/material/paginator';
import { Sort } from '@angular/material/sort'; import { Sort } from '@angular/material/sort';
import { AuthService } from 'src/app/services/auth.service'; import { AuthService } from 'src/app/services/auth.service';
import { UserManagementService } from 'src/app/services/user.management.service'; import { UserManagementService } from 'src/app/services/user.management.service';
import { ConfirmDialog } from 'src/app/ui/confirm/confirm.component';
@Component({ @Component({
selector: 'ui-users', selector: 'ui-users',
@@ -18,12 +16,10 @@ export class PageUsers implements OnInit {
pageSizeOptions: number[] = [1, 2, 3, 4, 5, 10, 15, 30, 50, 100]; pageSizeOptions: number[] = [1, 2, 3, 4, 5, 10, 15, 30, 50, 100];
sort: string = "username"; sort: string = "username";
descending: boolean = false; descending: boolean = false;
filterOpen: boolean = true;
columns: string[] = []; columns: string[] = [];
form: FormGroup;
isAdmin: boolean = false;
username: string = ""; username: string = "";
constructor( constructor(
@@ -35,12 +31,6 @@ export class PageUsers implements OnInit {
this.users = {}; this.users = {};
this.update(); this.update();
this.applyResize(window.innerWidth || document.documentElement.clientWidth || document.body.clientWidth); 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({ this.authService.auth.subscribe({
next: (auth) => { next: (auth) => {
@@ -66,9 +56,11 @@ export class PageUsers implements OnInit {
} }
update() { update() {
this.userManagementService.fetch(this.users.limit || 15, this.users.offset || 0, this.sort, this.descending).subscribe({ const filter = this.users.filter || "";
this.userManagementService.fetch(this.users.limit || 15, this.users.offset || 0, this.sort, this.descending, filter).subscribe({
next: (data: any) => { next: (data: any) => {
this.users = data; this.users = data;
this.users.filter = filter;
}, error: (error) => { }, error: (error) => {
this.users = { error: error }; this.users = { error: error };
} }
@@ -87,48 +79,13 @@ export class PageUsers implements OnInit {
this.update(); this.update();
} }
createUser() { setFilter(target: EventTarget) {
const user = { const value = (target as HTMLInputElement).value;
username: this.form.get("username").value, if (value != this.users.filter) {
name: this.form.get("name").value, this.users.filter = value;
email: this.form.get("email").value, this.users.offset = 0;
roles: this.isAdmin ? ['ROLE_ADMIN'] : [] this.update();
} }
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]);
}
}
}
})
} }
} }
@@ -12,8 +12,8 @@ export class UserManagementService {
constructor(private http: HttpClient, private abstractService: AbstractService) { constructor(private http: HttpClient, private abstractService: AbstractService) {
} }
fetch(limit: number, offset: number, sort: string, descending: boolean, filter: string = "") { fetch(limit: number, offset: number, sort: string, descending: boolean, search: string = "") {
return this.abstractService.fetch("/users/manage", limit, offset, sort, descending, filter); return this.abstractService.fetch("/users/manage", limit, offset, sort, descending, { filter: search });
} }
pick(search: string) { pick(search: string) {
@@ -239,7 +239,6 @@ export class UiMain {
} }
random() { random() {
console.log("Random?");
this.debugService.random().subscribe({ this.debugService.random().subscribe({
next: () => { next: () => {
window.location.reload(); window.location.reload();
@@ -15,10 +15,19 @@
} }
@if (turnovers) { @if (turnovers) {
@if(turnovers.results) {
@if (turnovers.total == 0) {
<mat-list>
<mat-list-item>
<p>{{'paginator.empty' | i18n}}</p>
</mat-list-item>
</mat-list>
}
@if(turnovers.total) {
<div class="scroll-container"> <div class="scroll-container">
<table class="default-table" mat-table [dataSource]="turnovers.results" matSort (matSortChange)="sort.emit($event)" <table class="default-table" mat-table [dataSource]="turnovers.results" matSort [matSortDisabled]="!enableSort"
[matSortDisableClear]="true"> (matSortChange)="sort.emit($event)" [matSortDisableClear]="true">
<ng-container matColumnDef="username"> <ng-container matColumnDef="username">
<th mat-header-cell *matHeaderCellDef mat-sort-header>{{'turnover.username' | i18n}}</th> <th mat-header-cell *matHeaderCellDef mat-sort-header>{{'turnover.username' | i18n}}</th>
@@ -114,13 +123,7 @@
</div> </div>
} }
@if (turnovers.total == 0) {
<mat-list>
<mat-list-item>
<p>{{'paginator.empty' | i18n}}</p>
</mat-list-item>
</mat-list>
}
<span class="spacer"></span> <span class="spacer"></span>
@@ -15,6 +15,7 @@ export class UiTurnovers implements OnInit {
@Input() showFilter: boolean = true; @Input() showFilter: boolean = true;
@Input() linkTurnover: boolean = true; @Input() linkTurnover: boolean = true;
@Input() username: boolean = false; @Input() username: boolean = false;
@Input() enableSort: boolean = true;
@Output() page: EventEmitter<PageEvent> = new EventEmitter<PageEvent>(); @Output() page: EventEmitter<PageEvent> = new EventEmitter<PageEvent>();
@Output() sort: EventEmitter<Sort> = new EventEmitter<Sort>(); @Output() sort: EventEmitter<Sort> = new EventEmitter<Sort>();
@@ -35,12 +36,14 @@ export class UiTurnovers implements OnInit {
applyResize(width: number) { applyResize(width: number) {
if (width < 992) { if (width < 992) {
this.columns = ['customer', 'motif']; this.columns = ['customer', 'price'];
} else { } else {
this.columns = ['customer', 'motif', 'price', 'timeInvestment', 'remark', 'materialConsumption', 'updated']; this.columns = ['customer', 'motif', 'price', 'timeInvestment', 'remark', 'materialConsumption', 'updated'];
} }
if (this.username) { if (this.username) {
this.columns.unshift('username'); this.columns.unshift('username');
} else {
this.columns.push('motif');
} }
this.columns.unshift('created'); this.columns.unshift('created');
} }
@@ -152,11 +152,18 @@
"confirmDelete": "Möchtest du den User '{0}' wirklich löschen? ALLE Buchungen dieses Users werden ebenfalls gelöscht!", "confirmDelete": "Möchtest du den User '{0}' wirklich löschen? ALLE Buchungen dieses Users werden ebenfalls gelöscht!",
"create": "Neuen User erstellen", "create": "Neuen User erstellen",
"delete": "User löschen", "delete": "User löschen",
"error": {
"ALREADY_EXISTS": "User existiert bereits"
},
"manage": "Verwalten", "manage": "Verwalten",
"password": "Passwort", "password": "Passwort",
"username": "User" "username": "User"
}, },
"users": { "users": {
".": "User verwalten" ".": "User verwalten",
"filter": {
".": "Filter",
"search": "Nach Username filtern"
}
} }
} }