Feature: Paged customer list.
This commit is contained in:
@ -2,11 +2,12 @@ import uuid
|
|||||||
|
|
||||||
from collections.abc import Sequence
|
from collections.abc import Sequence
|
||||||
from decimal import Decimal
|
from decimal import Decimal
|
||||||
|
from typing import Any
|
||||||
|
|
||||||
from fastapi import APIRouter, Depends, HTTPException, Security, status
|
from fastapi import APIRouter, Depends, HTTPException, Security, status
|
||||||
from sqlalchemy import delete, or_, select
|
from sqlalchemy import asc, delete, desc, func, or_, select
|
||||||
from sqlalchemy.exc import SQLAlchemyError
|
from sqlalchemy.exc import SQLAlchemyError
|
||||||
from sqlalchemy.orm import Session
|
from sqlalchemy.orm import InstrumentedAttribute, Session, contains_eager
|
||||||
|
|
||||||
from ..core.security import get_current_active_user as get_user
|
from ..core.security import get_current_active_user as get_user
|
||||||
from ..db.session import SessionFuture
|
from ..db.session import SessionFuture
|
||||||
@ -97,29 +98,99 @@ def show_blank(
|
|||||||
return blank_customer_info(sc)
|
return blank_customer_info(sc)
|
||||||
|
|
||||||
|
|
||||||
@router.get("/list", response_model=list[schemas.Customer])
|
@router.get("/list", response_model=schemas.PagedCustomers)
|
||||||
def show_list(user: UserToken = Depends(get_user)) -> list[schemas.Customer]:
|
def show_list(
|
||||||
|
q: str | None = None,
|
||||||
|
p: int = 0, # Page number
|
||||||
|
s: int = 50, # Page size
|
||||||
|
f: str | None = None, # Sort field
|
||||||
|
d: str | None = None, # Sort direction
|
||||||
|
user: UserToken = Depends(get_user),
|
||||||
|
) -> schemas.PagedCustomers:
|
||||||
|
# Handle dynamic sorting safely
|
||||||
|
sort_field = ALLOWED_SORT_FIELDS.get(f or "name", Customer.name) # Default to name
|
||||||
|
sort_direction = desc if (d or "").lower() == "desc" else asc
|
||||||
|
|
||||||
|
filter = [] if q is None else [x.strip() for x in q.split()]
|
||||||
with SessionFuture() as db:
|
with SessionFuture() as db:
|
||||||
sc = db.execute(select(SaleCategory).order_by(SaleCategory.name)).scalars().all()
|
sc = db.execute(select(SaleCategory).order_by(SaleCategory.name)).scalars().all()
|
||||||
return [
|
customers, total = customer_list(filter, p, s, sort_field, sort_direction, db)
|
||||||
customer_info(item, sc) for item in db.execute(select(Customer).order_by(Customer.name)).scalars().all()
|
return schemas.PagedCustomers(
|
||||||
]
|
items=[customer_info(item, sc) for item in customers],
|
||||||
|
page=p,
|
||||||
|
page_size=s,
|
||||||
|
total=total,
|
||||||
|
field=f,
|
||||||
|
sort_direction=d,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
ALLOWED_SORT_FIELDS: dict[str, InstrumentedAttribute[Any]] = {
|
||||||
|
"name": Customer.name,
|
||||||
|
"phone": Customer.phone,
|
||||||
|
"address": Customer.address,
|
||||||
|
"printInBill": Customer.print_in_bill,
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
@router.get("/query", response_model=list[schemas.Customer])
|
@router.get("/query", response_model=list[schemas.Customer])
|
||||||
def show_term(
|
def show_term(
|
||||||
q: str,
|
q: str | None,
|
||||||
|
p: int = 0, # Page number
|
||||||
|
s: int = 25, # Page size
|
||||||
|
f: str | None = None, # Sort field
|
||||||
|
d: str | None = None, # Sort direction
|
||||||
current_user: UserToken = Depends(get_user),
|
current_user: UserToken = Depends(get_user),
|
||||||
) -> list[schemas.Customer]:
|
) -> list[schemas.Customer]:
|
||||||
query = select(Customer)
|
# Handle dynamic sorting safely
|
||||||
if q is not None:
|
sort_field = ALLOWED_SORT_FIELDS.get(f or "name", Customer.name) # Default to name
|
||||||
for item in q.split():
|
sort_direction = desc if (d or "").lower() == "desc" else asc
|
||||||
query = query.where(or_(Customer.name.ilike(f"%{item}%"), Customer.phone.ilike(f"%{item}%")))
|
|
||||||
|
|
||||||
query = query.order_by(Customer.name)
|
filter = [] if q is None else [x.strip() for x in q.split()]
|
||||||
with SessionFuture() as db:
|
with SessionFuture() as db:
|
||||||
sc = db.execute(select(SaleCategory).order_by(SaleCategory.name)).scalars().all()
|
sc = db.execute(select(SaleCategory).order_by(SaleCategory.name)).scalars().all()
|
||||||
return [customer_info(item, sc) for item in db.execute(query).scalars().all()]
|
customers, _ = customer_list(filter, p, s, sort_field, sort_direction, db)
|
||||||
|
return [customer_info(item, sc) for item in customers]
|
||||||
|
|
||||||
|
|
||||||
|
def customer_list(
|
||||||
|
filter: list[str],
|
||||||
|
page: int | None,
|
||||||
|
page_size: int | None,
|
||||||
|
sort_field: InstrumentedAttribute[Any] | None,
|
||||||
|
sort_direction: Any | None,
|
||||||
|
db: Session,
|
||||||
|
) -> tuple[Sequence[Customer], int]:
|
||||||
|
# Handle dynamic sorting safely
|
||||||
|
sort_field = sort_field or Customer.name # Default to name
|
||||||
|
sort_direction = sort_direction or asc
|
||||||
|
|
||||||
|
print(
|
||||||
|
f"Filter: {filter}, Page: {page}, Page Size: {page_size}, Sort Field: {sort_field}, Sort Direction: {sort_direction}"
|
||||||
|
)
|
||||||
|
id_query = select(Customer.id)
|
||||||
|
for item in filter:
|
||||||
|
id_query = id_query.where(or_(Customer.name.ilike(f"%{item}%"), Customer.phone.ilike(f"%{item}%")))
|
||||||
|
id_query = id_query.order_by(sort_direction(sort_field))
|
||||||
|
if page_size: # Paging
|
||||||
|
if page:
|
||||||
|
id_query = id_query.offset(page * page_size)
|
||||||
|
id_query = id_query.limit(page_size)
|
||||||
|
ids = db.execute(id_query).scalars().all()
|
||||||
|
|
||||||
|
total_query = select(func.count(Customer.id))
|
||||||
|
for item in filter:
|
||||||
|
total_query = total_query.where(or_(Customer.name.ilike(f"%{item}%"), Customer.phone.ilike(f"%{item}%")))
|
||||||
|
total = db.execute(total_query).scalar_one()
|
||||||
|
|
||||||
|
query = (
|
||||||
|
select(Customer)
|
||||||
|
.join(Customer.discounts, isouter=True)
|
||||||
|
.where(Customer.id.in_(ids))
|
||||||
|
.order_by(sort_direction(sort_field))
|
||||||
|
.options(contains_eager(Customer.discounts))
|
||||||
|
)
|
||||||
|
return db.execute(query).unique().scalars().all(), total
|
||||||
|
|
||||||
|
|
||||||
@router.get("/{id_}", response_model=schemas.Customer)
|
@router.get("/{id_}", response_model=schemas.Customer)
|
||||||
|
|||||||
@ -40,3 +40,17 @@ class CustomerLink(BaseModel):
|
|||||||
class CustomerBlank(CustomerIn):
|
class CustomerBlank(CustomerIn):
|
||||||
name: str
|
name: str
|
||||||
model_config = ConfigDict(str_strip_whitespace=True, alias_generator=to_camel, populate_by_name=True)
|
model_config = ConfigDict(str_strip_whitespace=True, alias_generator=to_camel, populate_by_name=True)
|
||||||
|
|
||||||
|
|
||||||
|
class PagedCustomers(BaseModel):
|
||||||
|
items: list[Customer]
|
||||||
|
total: int
|
||||||
|
page: int
|
||||||
|
page_size: int
|
||||||
|
field: str | None
|
||||||
|
sort_direction: str | None
|
||||||
|
|
||||||
|
model_config = ConfigDict(str_strip_whitespace=True, alias_generator=to_camel, populate_by_name=True)
|
||||||
|
|
||||||
|
def __len__(self) -> int:
|
||||||
|
return len(self.items)
|
||||||
|
|||||||
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 { ResolveFn } from '@angular/router';
|
||||||
|
|
||||||
import { Customer } from '../core/customer';
|
import { Customer } from '../core/customer';
|
||||||
|
import { PagedResult } from '../core/paged-result';
|
||||||
import { CustomerService } from './customer.service';
|
import { CustomerService } from './customer.service';
|
||||||
|
|
||||||
export const customerListResolver: ResolveFn<Customer[]> = () => {
|
export const customerListResolver: ResolveFn<PagedResult<Customer>> = (route) => {
|
||||||
return inject(CustomerService).list();
|
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 { 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 { Observable, of as observableOf } from 'rxjs';
|
||||||
|
import { PagedResult } from 'src/app/core/paged-result';
|
||||||
|
|
||||||
import { Customer } from '../../core/customer';
|
import { Customer } from '../../core/customer';
|
||||||
|
|
||||||
export class CustomerListDatasource extends DataSource<Customer> {
|
export class CustomerListDatasource extends DataSource<Customer> {
|
||||||
constructor(public data: Customer[]) {
|
constructor(
|
||||||
|
public data: PagedResult<Customer>,
|
||||||
|
private paginator?: MatPaginator,
|
||||||
|
private sort?: MatSort,
|
||||||
|
) {
|
||||||
super();
|
super();
|
||||||
}
|
}
|
||||||
|
|
||||||
connect(): Observable<Customer[]> {
|
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() {}
|
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-title-group>
|
||||||
</mat-card-header>
|
</mat-card-header>
|
||||||
<mat-card-content>
|
<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 -->
|
<!-- Name Column -->
|
||||||
<ng-container matColumnDef="name">
|
<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"
|
<mat-cell *matCellDef="let row"
|
||||||
><a [routerLink]="['/customers', row.id]">{{ row.name }}</a></mat-cell
|
><a [routerLink]="['/customers', row.id]">{{ row.name }}</a></mat-cell
|
||||||
>
|
>
|
||||||
@ -20,19 +28,19 @@
|
|||||||
|
|
||||||
<!-- Phone Column -->
|
<!-- Phone Column -->
|
||||||
<ng-container matColumnDef="phone">
|
<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>
|
<mat-cell *matCellDef="let row">{{ row.phone }}</mat-cell>
|
||||||
</ng-container>
|
</ng-container>
|
||||||
|
|
||||||
<!-- Address Column -->
|
<!-- Address Column -->
|
||||||
<ng-container matColumnDef="address">
|
<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>
|
<mat-cell *matCellDef="let row">{{ row.address }}</mat-cell>
|
||||||
</ng-container>
|
</ng-container>
|
||||||
|
|
||||||
<!-- Print In Bill Column -->
|
<!-- Print In Bill Column -->
|
||||||
<ng-container matColumnDef="printInBill">
|
<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>
|
<mat-cell *matCellDef="let row">{{ row.printInBill }}</mat-cell>
|
||||||
</ng-container>
|
</ng-container>
|
||||||
|
|
||||||
@ -51,5 +59,13 @@
|
|||||||
<mat-header-row *matHeaderRowDef="displayedColumns"></mat-header-row>
|
<mat-header-row *matHeaderRowDef="displayedColumns"></mat-header-row>
|
||||||
<mat-row *matRowDef="let row; columns: displayedColumns"></mat-row>
|
<mat-row *matRowDef="let row; columns: displayedColumns"></mat-row>
|
||||||
</mat-table>
|
</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-content>
|
||||||
</mat-card>
|
</mat-card>
|
||||||
|
|||||||
@ -1,10 +1,16 @@
|
|||||||
import { PercentPipe } from '@angular/common';
|
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 { MatButtonModule } from '@angular/material/button';
|
||||||
import { MatCardModule } from '@angular/material/card';
|
import { MatCardModule } from '@angular/material/card';
|
||||||
import { MatIconModule } from '@angular/material/icon';
|
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 { 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 { Customer } from '../../core/customer';
|
||||||
import { CustomerListDatasource } from './customer-list-datasource';
|
import { CustomerListDatasource } from './customer-list-datasource';
|
||||||
@ -13,22 +19,88 @@ import { CustomerListDatasource } from './customer-list-datasource';
|
|||||||
selector: 'app-customer-list',
|
selector: 'app-customer-list',
|
||||||
templateUrl: './customer-list.component.html',
|
templateUrl: './customer-list.component.html',
|
||||||
styleUrls: ['./customer-list.component.css'],
|
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 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. */
|
/** Columns displayed in the table. Columns IDs can be added, removed, or reordered. */
|
||||||
displayedColumns = ['name', 'phone', 'address', 'printInBill', 'discounts'];
|
displayedColumns = ['name', 'phone', 'address', 'printInBill', 'discounts'];
|
||||||
|
|
||||||
ngOnInit() {
|
constructor() {
|
||||||
this.route.data.subscribe((value) => {
|
this.form = new FormGroup({
|
||||||
const data = value as { list: Customer[] };
|
filter: new FormControl<string>('', { nonNullable: true }),
|
||||||
data.list.forEach((c) => (c.discounts = c.discounts.filter((d) => d.discount !== 0)));
|
|
||||||
this.list = data.list;
|
|
||||||
});
|
});
|
||||||
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',
|
permission: 'Customers',
|
||||||
},
|
},
|
||||||
resolve: {
|
resolve: {
|
||||||
list: customerListResolver,
|
data: customerListResolver,
|
||||||
},
|
},
|
||||||
|
runGuardsAndResolvers: 'always',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: 'new',
|
path: 'new',
|
||||||
|
|||||||
@ -5,6 +5,7 @@ import { catchError } from 'rxjs/operators';
|
|||||||
|
|
||||||
import { Customer } from '../core/customer';
|
import { Customer } from '../core/customer';
|
||||||
import { ErrorLoggerService } from '../core/error-logger.service';
|
import { ErrorLoggerService } from '../core/error-logger.service';
|
||||||
|
import { PagedResult } from '../core/paged-result';
|
||||||
|
|
||||||
const httpOptions = {
|
const httpOptions = {
|
||||||
headers: new HttpHeaders({ 'Content-Type': 'application/json' }),
|
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>;
|
.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
|
return this.http
|
||||||
.get<Customer[]>(`${url}/list`)
|
.get<PagedResult<Customer>>(`${url}/list`, { params })
|
||||||
.pipe(catchError(this.log.handleError(serviceName, 'list'))) as Observable<Customer[]>;
|
.pipe(catchError(this.log.handleError(serviceName, 'list'))) as Observable<PagedResult<Customer>>;
|
||||||
}
|
}
|
||||||
|
|
||||||
save(customer: Customer): Observable<Customer> {
|
save(customer: Customer): Observable<Customer> {
|
||||||
|
|||||||
Reference in New Issue
Block a user