Feature: Paged customer list.
This commit is contained in:
17
bookie/src/app/core/paged-result.ts
Normal file
17
bookie/src/app/core/paged-result.ts
Normal file
@ -0,0 +1,17 @@
|
||||
export class PagedResult<T> {
|
||||
items: T[];
|
||||
page: number; // current page (zero-based)
|
||||
pageSize: number; // items per page
|
||||
total: number; // total items in all pages
|
||||
totalPages: number;
|
||||
field?: string; // active sort column
|
||||
sortDirection?: 'asc' | 'desc'; // sort direction
|
||||
|
||||
constructor(items: T[], page: number, pageSize: number, total: number) {
|
||||
this.items = items;
|
||||
this.page = page;
|
||||
this.pageSize = pageSize;
|
||||
this.total = total;
|
||||
this.totalPages = Math.ceil(total / pageSize);
|
||||
}
|
||||
}
|
||||
@ -2,8 +2,14 @@ import { inject } from '@angular/core';
|
||||
import { ResolveFn } from '@angular/router';
|
||||
|
||||
import { Customer } from '../core/customer';
|
||||
import { PagedResult } from '../core/paged-result';
|
||||
import { CustomerService } from './customer.service';
|
||||
|
||||
export const customerListResolver: ResolveFn<Customer[]> = () => {
|
||||
return inject(CustomerService).list();
|
||||
export const customerListResolver: ResolveFn<PagedResult<Customer>> = (route) => {
|
||||
const q = route.queryParamMap.get('q');
|
||||
const page = Number(route.queryParamMap.get('p') ?? 0);
|
||||
const size = Number(route.queryParamMap.get('s') ?? 50);
|
||||
const field = route.queryParamMap.get('f') ?? 'name';
|
||||
const direction = route.queryParamMap.get('d') ?? 'asc';
|
||||
return inject(CustomerService).list(q, page, size, field, direction);
|
||||
};
|
||||
|
||||
@ -1,16 +1,161 @@
|
||||
import { DataSource } from '@angular/cdk/collections';
|
||||
import { MatPaginator } from '@angular/material/paginator';
|
||||
import { MatSort } from '@angular/material/sort';
|
||||
import { Observable, of as observableOf } from 'rxjs';
|
||||
import { PagedResult } from 'src/app/core/paged-result';
|
||||
|
||||
import { Customer } from '../../core/customer';
|
||||
|
||||
export class CustomerListDatasource extends DataSource<Customer> {
|
||||
constructor(public data: Customer[]) {
|
||||
constructor(
|
||||
public data: PagedResult<Customer>,
|
||||
private paginator?: MatPaginator,
|
||||
private sort?: MatSort,
|
||||
) {
|
||||
super();
|
||||
}
|
||||
|
||||
connect(): Observable<Customer[]> {
|
||||
return observableOf(this.data);
|
||||
if (this.paginator) {
|
||||
this.paginator.length = this.data.total;
|
||||
if (this.data.pageSize !== undefined) {
|
||||
this.paginator.pageSize = this.data.pageSize;
|
||||
}
|
||||
if (this.data.page !== undefined) {
|
||||
this.paginator.pageIndex = this.data.page;
|
||||
}
|
||||
}
|
||||
if (this.sort) {
|
||||
if (this.data.field !== undefined) {
|
||||
this.sort.active = this.data.field;
|
||||
}
|
||||
if (this.data.sortDirection !== undefined) {
|
||||
this.sort.direction = this.data.sortDirection;
|
||||
}
|
||||
}
|
||||
return observableOf(this.data.items);
|
||||
}
|
||||
|
||||
disconnect() {}
|
||||
}
|
||||
|
||||
// import { DataSource } from '@angular/cdk/collections';
|
||||
// import { map, merge, Observable, of as observableOf, tap } from 'rxjs';
|
||||
|
||||
// import { Customer } from '../../core/customer';
|
||||
// import { MatPaginator, PageEvent } from '@angular/material/paginator';
|
||||
// import { MatSort, Sort } from '@angular/material/sort';
|
||||
// import { EventEmitter } from '@angular/core';
|
||||
|
||||
// export class CustomerListDatasource extends DataSource<Customer> {
|
||||
// private filterValue = '';
|
||||
// constructor(public data: Customer[],
|
||||
// private readonly filter: Observable<string>,
|
||||
// private paginator?: MatPaginator,
|
||||
// private sort?: MatSort,
|
||||
|
||||
// ) {
|
||||
// super();
|
||||
// this.filter = filter.pipe(
|
||||
// tap((x) => {
|
||||
// this.filterValue = x;
|
||||
// }),
|
||||
// );
|
||||
// }
|
||||
|
||||
// connect(): Observable<Customer[]> {
|
||||
// const dataMutations: (EventEmitter<PageEvent> | EventEmitter<Sort>)[] = [];
|
||||
// if (this.paginator) {
|
||||
// dataMutations.push((this.paginator as MatPaginator).page);
|
||||
// }
|
||||
// if (this.sort) {
|
||||
// dataMutations.push((this.sort as MatSort).sortChange);
|
||||
// }
|
||||
|
||||
// return merge(observableOf(this.data), this.filter, ...dataMutations).pipe(
|
||||
// map(() => this.getFilteredData([...this.data])),
|
||||
// tap((x: Customer[]) => {
|
||||
// if (this.paginator) {
|
||||
// this.paginator.length = x.length;
|
||||
// }
|
||||
// }),
|
||||
// map((x: Customer[]) => this.getPagedData(this.getSortedData(x))),
|
||||
// );
|
||||
// }
|
||||
|
||||
// disconnect() {}
|
||||
// private getFilteredData(data: Customer[]): Customer[] {
|
||||
// return this.filterValue.toLowerCase().split(' ').reduce(
|
||||
// (p: Customer[], c: string) =>
|
||||
// p.filter((x) => {
|
||||
// const productString = `${x.name} ${x.phone}${x.address}`.toLowerCase();
|
||||
// return productString.indexOf(c) !== -1;
|
||||
// }),
|
||||
// Object.assign([], data),
|
||||
// );
|
||||
// }
|
||||
// private getPagedData(data: Customer[]) {
|
||||
// if (this.paginator === undefined) {
|
||||
// return data;
|
||||
// }
|
||||
// const startIndex = this.paginator.pageIndex * this.paginator.pageSize;
|
||||
// return data.splice(startIndex, this.paginator.pageSize);
|
||||
// }
|
||||
|
||||
// private getSortedData(data: Customer[]) {
|
||||
// if (this.sort === undefined) {
|
||||
// return data;
|
||||
// }
|
||||
// if (!this.sort.active || this.sort.direction === '') {
|
||||
// return data;
|
||||
// }
|
||||
|
||||
// const sort = this.sort as MatSort;
|
||||
// console.log('Sorting by', sort.active, sort.direction);
|
||||
// return data.sort(sortBy(sort.active as keyof Customer, sort.direction === 'asc'));
|
||||
// }
|
||||
|
||||
// }
|
||||
|
||||
// function compareValues<T>(a: T, b: T, isAsc: boolean): number {
|
||||
// if (a === b) return 0;
|
||||
// if (a == null) return isAsc ? -1 : 1;
|
||||
// if (b == null) return isAsc ? 1 : -1;
|
||||
|
||||
// if (typeof a === "string" && typeof b === "string") {
|
||||
// return isAsc ? a.localeCompare(b) : b.localeCompare(a);
|
||||
// }
|
||||
// if (typeof a === "number" && typeof b === "number") {
|
||||
// return isAsc ? a - b : b - a;
|
||||
// }
|
||||
// if (typeof a === "boolean" && typeof b === "boolean") {
|
||||
// return isAsc ? (Number(a) - Number(b)) : (Number(b) - Number(a));
|
||||
// }
|
||||
// // Fallback
|
||||
// const aStr = String(a);
|
||||
// const bStr = String(b);
|
||||
// return isAsc ? aStr.localeCompare(bStr) : bStr.localeCompare(aStr);
|
||||
// }
|
||||
|
||||
// function sortBy<T, K extends keyof T>(
|
||||
// key: K,
|
||||
// isAsc = true
|
||||
// ): (a: T, b: T) => number {
|
||||
// return (a, b) => compareValues(a[key], b[key], isAsc);
|
||||
// }
|
||||
|
||||
// // function getNestedValue<T>(obj: T, path: string | string[]): any {
|
||||
// // const keys = Array.isArray(path) ? path : path.split(".");
|
||||
// // return keys.reduce((acc, key) => (acc == null ? undefined : acc[key]), obj);
|
||||
// // }
|
||||
|
||||
// // function sortByNested<T>(
|
||||
// // path: string | string[],
|
||||
// // isAsc = true
|
||||
// // ): (a: T, b: T) => number {
|
||||
// // return (a, b) => {
|
||||
// // const aValue = getNestedValue(a, path);
|
||||
// // const bValue = getNestedValue(b, path);
|
||||
// // return compareValues(aValue, bValue, isAsc);
|
||||
// // };
|
||||
// // }
|
||||
|
||||
@ -9,10 +9,18 @@
|
||||
</mat-card-title-group>
|
||||
</mat-card-header>
|
||||
<mat-card-content>
|
||||
<mat-table #table [dataSource]="dataSource" aria-label="Elements">
|
||||
<form [formGroup]="form" class="flex flex-col">
|
||||
<div class="flex flex-row justify-around content-start items-start">
|
||||
<mat-form-field class="flex-auto">
|
||||
<mat-label>Filter</mat-label>
|
||||
<input type="text" matInput #filterElement formControlName="filter" autocomplete="off" />
|
||||
</mat-form-field>
|
||||
</div>
|
||||
</form>
|
||||
<mat-table #table [dataSource]="dataSource" matSort aria-label="Elements">
|
||||
<!-- Name Column -->
|
||||
<ng-container matColumnDef="name">
|
||||
<mat-header-cell *matHeaderCellDef>Name</mat-header-cell>
|
||||
<mat-header-cell *matHeaderCellDef mat-sort-header>Name</mat-header-cell>
|
||||
<mat-cell *matCellDef="let row"
|
||||
><a [routerLink]="['/customers', row.id]">{{ row.name }}</a></mat-cell
|
||||
>
|
||||
@ -20,19 +28,19 @@
|
||||
|
||||
<!-- Phone Column -->
|
||||
<ng-container matColumnDef="phone">
|
||||
<mat-header-cell *matHeaderCellDef>Phone</mat-header-cell>
|
||||
<mat-header-cell *matHeaderCellDef mat-sort-header>Phone</mat-header-cell>
|
||||
<mat-cell *matCellDef="let row">{{ row.phone }}</mat-cell>
|
||||
</ng-container>
|
||||
|
||||
<!-- Address Column -->
|
||||
<ng-container matColumnDef="address">
|
||||
<mat-header-cell *matHeaderCellDef>Address</mat-header-cell>
|
||||
<mat-header-cell *matHeaderCellDef mat-sort-header>Address</mat-header-cell>
|
||||
<mat-cell *matCellDef="let row">{{ row.address }}</mat-cell>
|
||||
</ng-container>
|
||||
|
||||
<!-- Print In Bill Column -->
|
||||
<ng-container matColumnDef="printInBill">
|
||||
<mat-header-cell *matHeaderCellDef>Print in Bill</mat-header-cell>
|
||||
<mat-header-cell *matHeaderCellDef mat-sort-header>Print in Bill</mat-header-cell>
|
||||
<mat-cell *matCellDef="let row">{{ row.printInBill }}</mat-cell>
|
||||
</ng-container>
|
||||
|
||||
@ -51,5 +59,13 @@
|
||||
<mat-header-row *matHeaderRowDef="displayedColumns"></mat-header-row>
|
||||
<mat-row *matRowDef="let row; columns: displayedColumns"></mat-row>
|
||||
</mat-table>
|
||||
<mat-paginator
|
||||
#paginator
|
||||
[length]="dataSource.data.total"
|
||||
[pageIndex]="0"
|
||||
[pageSize]="50"
|
||||
[pageSizeOptions]="[25, 50, 100, 250, 5000]"
|
||||
>
|
||||
</mat-paginator>
|
||||
</mat-card-content>
|
||||
</mat-card>
|
||||
|
||||
@ -1,10 +1,16 @@
|
||||
import { PercentPipe } from '@angular/common';
|
||||
import { Component, OnInit, inject } from '@angular/core';
|
||||
import { ChangeDetectorRef, Component, ElementRef, OnInit, ViewChild, inject, AfterViewInit } from '@angular/core';
|
||||
import { FormControl, FormGroup, ReactiveFormsModule } from '@angular/forms';
|
||||
import { MatButtonModule } from '@angular/material/button';
|
||||
import { MatCardModule } from '@angular/material/card';
|
||||
import { MatIconModule } from '@angular/material/icon';
|
||||
import { MatInputModule } from '@angular/material/input';
|
||||
import { MatPaginator, MatPaginatorModule } from '@angular/material/paginator';
|
||||
import { MatSort, MatSortModule } from '@angular/material/sort';
|
||||
import { MatTableModule } from '@angular/material/table';
|
||||
import { ActivatedRoute, RouterLink } from '@angular/router';
|
||||
import { ActivatedRoute, Router, RouterLink } from '@angular/router';
|
||||
import { debounceTime, distinctUntilChanged, Observable } from 'rxjs';
|
||||
import { PagedResult } from 'src/app/core/paged-result';
|
||||
|
||||
import { Customer } from '../../core/customer';
|
||||
import { CustomerListDatasource } from './customer-list-datasource';
|
||||
@ -13,22 +19,88 @@ import { CustomerListDatasource } from './customer-list-datasource';
|
||||
selector: 'app-customer-list',
|
||||
templateUrl: './customer-list.component.html',
|
||||
styleUrls: ['./customer-list.component.css'],
|
||||
imports: [MatButtonModule, MatCardModule, MatIconModule, MatTableModule, PercentPipe, RouterLink],
|
||||
imports: [
|
||||
MatButtonModule,
|
||||
MatCardModule,
|
||||
MatIconModule,
|
||||
MatTableModule,
|
||||
PercentPipe,
|
||||
RouterLink,
|
||||
MatInputModule,
|
||||
MatPaginatorModule,
|
||||
MatSortModule,
|
||||
ReactiveFormsModule,
|
||||
],
|
||||
})
|
||||
export class CustomerListComponent implements OnInit {
|
||||
export class CustomerListComponent implements OnInit, AfterViewInit {
|
||||
private route = inject(ActivatedRoute);
|
||||
private router = inject(Router);
|
||||
private cdr = inject(ChangeDetectorRef);
|
||||
|
||||
@ViewChild('filterElement', { static: true }) filterElement!: ElementRef<HTMLInputElement>;
|
||||
@ViewChild(MatPaginator, { static: true }) paginator!: MatPaginator;
|
||||
@ViewChild(MatSort, { static: true }) sort!: MatSort;
|
||||
|
||||
data: PagedResult<Customer> = new PagedResult<Customer>([], 0, 0, 0);
|
||||
filter: Observable<string>;
|
||||
dataSource: CustomerListDatasource;
|
||||
form: FormGroup<{
|
||||
filter: FormControl<string>;
|
||||
}>;
|
||||
|
||||
dataSource: CustomerListDatasource = new CustomerListDatasource([]);
|
||||
list: Customer[] = [];
|
||||
/** Columns displayed in the table. Columns IDs can be added, removed, or reordered. */
|
||||
displayedColumns = ['name', 'phone', 'address', 'printInBill', 'discounts'];
|
||||
|
||||
ngOnInit() {
|
||||
this.route.data.subscribe((value) => {
|
||||
const data = value as { list: Customer[] };
|
||||
data.list.forEach((c) => (c.discounts = c.discounts.filter((d) => d.discount !== 0)));
|
||||
this.list = data.list;
|
||||
constructor() {
|
||||
this.form = new FormGroup({
|
||||
filter: new FormControl<string>('', { nonNullable: true }),
|
||||
});
|
||||
this.dataSource = new CustomerListDatasource(this.list);
|
||||
this.filter = this.form.controls.filter.valueChanges.pipe(debounceTime(150), distinctUntilChanged());
|
||||
this.dataSource = new CustomerListDatasource(this.data, this.paginator, this.sort);
|
||||
}
|
||||
|
||||
ngOnInit() {
|
||||
const q = this.route.snapshot.queryParamMap.get('q');
|
||||
if (q) {
|
||||
this.form.controls.filter.setValue(q, { emitEvent: false }); // Prevent infinite loop
|
||||
}
|
||||
this.route.data.subscribe((value) => {
|
||||
const data = value as { data: PagedResult<Customer> };
|
||||
data.data.items.forEach((c) => (c.discounts = c.discounts.filter((d) => d.discount !== 0)));
|
||||
this.data = data.data;
|
||||
this.dataSource = new CustomerListDatasource(this.data, this.paginator, this.sort);
|
||||
this.cdr.detectChanges();
|
||||
});
|
||||
this.sort.sortChange.subscribe({
|
||||
next: () =>
|
||||
this.router.navigate([], {
|
||||
relativeTo: this.route,
|
||||
queryParams: { f: this.sort?.active ?? 'name', d: this.sort?.direction },
|
||||
queryParamsHandling: 'merge',
|
||||
}),
|
||||
});
|
||||
this.paginator.page.subscribe({
|
||||
next: () =>
|
||||
this.router.navigate([], {
|
||||
relativeTo: this.route,
|
||||
queryParams: { s: this.paginator.pageSize, p: this.paginator.pageIndex },
|
||||
queryParamsHandling: 'merge',
|
||||
}),
|
||||
});
|
||||
this.filter.subscribe({
|
||||
next: (value) => {
|
||||
this.router.navigate([], {
|
||||
relativeTo: this.route,
|
||||
queryParams: { q: value },
|
||||
queryParamsHandling: 'merge',
|
||||
});
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
ngAfterViewInit() {
|
||||
setTimeout(() => {
|
||||
this.filterElement.nativeElement.focus();
|
||||
}, 0);
|
||||
}
|
||||
}
|
||||
|
||||
@ -15,8 +15,9 @@ export const routes: Routes = [
|
||||
permission: 'Customers',
|
||||
},
|
||||
resolve: {
|
||||
list: customerListResolver,
|
||||
data: customerListResolver,
|
||||
},
|
||||
runGuardsAndResolvers: 'always',
|
||||
},
|
||||
{
|
||||
path: 'new',
|
||||
|
||||
@ -5,6 +5,7 @@ import { catchError } from 'rxjs/operators';
|
||||
|
||||
import { Customer } from '../core/customer';
|
||||
import { ErrorLoggerService } from '../core/error-logger.service';
|
||||
import { PagedResult } from '../core/paged-result';
|
||||
|
||||
const httpOptions = {
|
||||
headers: new HttpHeaders({ 'Content-Type': 'application/json' }),
|
||||
@ -26,10 +27,18 @@ export class CustomerService {
|
||||
.pipe(catchError(this.log.handleError(serviceName, `get id=${id}`))) as Observable<Customer>;
|
||||
}
|
||||
|
||||
list(): Observable<Customer[]> {
|
||||
list(q: string | null, page = 0, size = 50, field = 'name', direction = 'asc'): Observable<PagedResult<Customer>> {
|
||||
const params = {
|
||||
...(q ? { q } : {}),
|
||||
p: page,
|
||||
s: size,
|
||||
f: field,
|
||||
d: direction,
|
||||
};
|
||||
console.log('CustomerService.list', params);
|
||||
return this.http
|
||||
.get<Customer[]>(`${url}/list`)
|
||||
.pipe(catchError(this.log.handleError(serviceName, 'list'))) as Observable<Customer[]>;
|
||||
.get<PagedResult<Customer>>(`${url}/list`, { params })
|
||||
.pipe(catchError(this.log.handleError(serviceName, 'list'))) as Observable<PagedResult<Customer>>;
|
||||
}
|
||||
|
||||
save(customer: Customer): Observable<Customer> {
|
||||
|
||||
Reference in New Issue
Block a user