Consolidated commit message to v22 signals
This commit is contained in:
@@ -1,54 +1,61 @@
|
||||
<h2 class="row-container space-between wrapped">
|
||||
<span>Account</span>
|
||||
<mat-icon matSuffix (click)="item.isStarred = !item.isStarred" class="pointer" [class.gold]="item.isStarred">
|
||||
{{ item.isStarred ? 'star' : 'star_border' }}
|
||||
<mat-icon matSuffix (click)="item().isStarred = !item().isStarred" class="pointer" [class.gold]="item().isStarred">
|
||||
{{ item().isStarred ? 'star' : 'star_border' }}
|
||||
</mat-icon>
|
||||
</h2>
|
||||
<form [formGroup]="form" class="flex-col wrapped">
|
||||
@if (itemResource.isLoading()) {
|
||||
<app-skeleton-loader type="card" [count]="1" />
|
||||
<app-skeleton-loader type="line" [count]="10" />
|
||||
} @else if (itemResource.error()) {
|
||||
<app-error-state [message]="'Failed to load data.'" (retryAction)="itemResource.reload()" />
|
||||
} @else {
|
||||
<form class="flex-col wrapped" [formRoot]="accountForm">
|
||||
<div class="row-container">
|
||||
<mat-form-field class="flex-auto">
|
||||
<mat-label>Code</mat-label>
|
||||
<input matInput [formField]="accountForm.code" />
|
||||
</mat-form-field>
|
||||
</div>
|
||||
<div class="row-container">
|
||||
<mat-form-field class="flex-auto">
|
||||
<mat-label>Name</mat-label>
|
||||
<input matInput [formField]="accountForm.name" />
|
||||
</mat-form-field>
|
||||
</div>
|
||||
<div class="row-container">
|
||||
<mat-form-field class="flex-auto">
|
||||
<mat-label>Account Type</mat-label>
|
||||
<mat-select [formField]="accountForm.type">
|
||||
@for (at of accountTypes(); track at) {
|
||||
<mat-option [value]="at.id">
|
||||
{{ at.name }}
|
||||
</mat-option>
|
||||
}
|
||||
</mat-select>
|
||||
</mat-form-field>
|
||||
</div>
|
||||
<div class="row-container">
|
||||
<mat-checkbox [formField]="accountForm.isActive" class="flex-auto">Is Active?</mat-checkbox>
|
||||
<mat-checkbox [formField]="accountForm.isReconcilable" class="flex-auto">Is Reconcilable?</mat-checkbox>
|
||||
</div>
|
||||
<div class="row-container">
|
||||
<mat-form-field class="flex-auto">
|
||||
<mat-label>Cost Centre</mat-label>
|
||||
<mat-select [formField]="accountForm.costCentre">
|
||||
@for (cs of costCentres(); track cs) {
|
||||
<mat-option [value]="cs.id">
|
||||
{{ cs.name }}
|
||||
</mat-option>
|
||||
}
|
||||
</mat-select>
|
||||
</mat-form-field>
|
||||
</div>
|
||||
</form>
|
||||
<div class="row-container">
|
||||
<mat-form-field class="flex-auto">
|
||||
<mat-label>Code</mat-label>
|
||||
<input matInput formControlName="code" />
|
||||
</mat-form-field>
|
||||
<button mat-raised-button color="primary" (click)="save()" class="">Save</button>
|
||||
@if (!!item().id) {
|
||||
<button mat-raised-button color="warn" (click)="confirmDelete()">Delete</button>
|
||||
}
|
||||
</div>
|
||||
<div class="row-container">
|
||||
<mat-form-field class="flex-auto">
|
||||
<mat-label>Name</mat-label>
|
||||
<input matInput #nameElement formControlName="name" />
|
||||
</mat-form-field>
|
||||
</div>
|
||||
<div class="row-container">
|
||||
<mat-form-field class="flex-auto">
|
||||
<mat-label>Account Type</mat-label>
|
||||
<mat-select formControlName="type">
|
||||
@for (at of accountTypes; track at) {
|
||||
<mat-option [value]="at.id">
|
||||
{{ at.name }}
|
||||
</mat-option>
|
||||
}
|
||||
</mat-select>
|
||||
</mat-form-field>
|
||||
</div>
|
||||
<div class="row-container">
|
||||
<mat-checkbox formControlName="isActive" class="flex-auto">Is Active?</mat-checkbox>
|
||||
<mat-checkbox formControlName="isReconcilable" class="flex-auto">Is Reconcilable?</mat-checkbox>
|
||||
</div>
|
||||
<div class="row-container">
|
||||
<mat-form-field class="flex-auto">
|
||||
<mat-label>Cost Centre</mat-label>
|
||||
<mat-select formControlName="costCentre">
|
||||
@for (cs of costCentres; track cs) {
|
||||
<mat-option [value]="cs.id">
|
||||
{{ cs.name }}
|
||||
</mat-option>
|
||||
}
|
||||
</mat-select>
|
||||
</mat-form-field>
|
||||
</div>
|
||||
</form>
|
||||
<div class="row-container">
|
||||
<button mat-raised-button color="primary" (click)="save()" class="">Save</button>
|
||||
@if (!!item.id) {
|
||||
<button mat-raised-button color="warn" (click)="confirmDelete()">Delete</button>
|
||||
}
|
||||
</div>
|
||||
}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { AfterViewInit, Component, ElementRef, inject, OnInit, ViewChild } from '@angular/core';
|
||||
import { FormControl, FormGroup, ReactiveFormsModule } from '@angular/forms';
|
||||
import { Component, inject, afterNextRender, linkedSignal, input, computed } from '@angular/core';
|
||||
import { form, FormField, FormRoot } from '@angular/forms/signals';
|
||||
import { MatButtonModule } from '@angular/material/button';
|
||||
import { MatCheckboxModule } from '@angular/material/checkbox';
|
||||
import { MatOptionModule } from '@angular/material/core';
|
||||
@@ -9,13 +9,24 @@ import { MatIconModule } from '@angular/material/icon';
|
||||
import { MatInputModule } from '@angular/material/input';
|
||||
import { MatSelectModule } from '@angular/material/select';
|
||||
import { MatSnackBar } from '@angular/material/snack-bar';
|
||||
import { ActivatedRoute, Router } from '@angular/router';
|
||||
import { Router } from '@angular/router';
|
||||
|
||||
import { Account } from '../../core/account';
|
||||
import { AccountType } from '../../core/account-type';
|
||||
import { AccountService } from '../../core/account.service';
|
||||
import { CostCentre } from '../../core/cost-centre';
|
||||
import { CostCentreService } from '../../cost-centre/cost-centre.service';
|
||||
import { ConfirmDialogComponent } from '../../shared/confirm-dialog/confirm-dialog.component';
|
||||
import { ErrorStateComponent } from '../../shared/error-state/error-state.component';
|
||||
import { SkeletonLoaderComponent } from '../../shared/skeleton-loader/skeleton-loader.component';
|
||||
import { AccountTypeService } from '../account-type.service';
|
||||
|
||||
export interface AccountDetailFormData {
|
||||
code: string;
|
||||
name: string;
|
||||
type: number;
|
||||
isActive: boolean;
|
||||
isReconcilable: boolean;
|
||||
costCentre: string;
|
||||
}
|
||||
|
||||
@Component({
|
||||
selector: 'app-account-detail',
|
||||
@@ -23,79 +34,59 @@ import { ConfirmDialogComponent } from '../../shared/confirm-dialog/confirm-dial
|
||||
styleUrls: ['./account-detail.component.css'],
|
||||
imports: [
|
||||
MatIconModule,
|
||||
ReactiveFormsModule,
|
||||
MatFormFieldModule,
|
||||
MatInputModule,
|
||||
MatSelectModule,
|
||||
MatOptionModule,
|
||||
MatCheckboxModule,
|
||||
MatButtonModule,
|
||||
FormField,
|
||||
FormRoot,
|
||||
|
||||
SkeletonLoaderComponent,
|
||||
ErrorStateComponent,
|
||||
],
|
||||
})
|
||||
export class AccountDetailComponent implements OnInit, AfterViewInit {
|
||||
private route = inject(ActivatedRoute);
|
||||
export class AccountDetailComponent {
|
||||
private router = inject(Router);
|
||||
private dialog = inject(MatDialog);
|
||||
private snackBar = inject(MatSnackBar);
|
||||
private ser = inject(AccountService);
|
||||
private accountTypeService = inject(AccountTypeService);
|
||||
private costCentreService = inject(CostCentreService);
|
||||
id = input(null, { transform: (v: string | null | undefined) => v ?? null });
|
||||
|
||||
@ViewChild('nameElement', { static: true }) nameElement!: ElementRef<HTMLInputElement>;
|
||||
form: FormGroup<{
|
||||
code: FormControl<number | string>;
|
||||
name: FormControl<string | null>;
|
||||
type: FormControl<number>;
|
||||
isActive: FormControl<boolean>;
|
||||
isReconcilable: FormControl<boolean>;
|
||||
costCentre: FormControl<string | null>;
|
||||
}>;
|
||||
itemResource = this.ser.get(this.id);
|
||||
|
||||
accountTypes: AccountType[] = [];
|
||||
costCentres: CostCentre[] = [];
|
||||
item: Account = new Account();
|
||||
item = computed(() => this.itemResource.value() ?? new Account());
|
||||
|
||||
accountModel = linkedSignal({
|
||||
source: this.item,
|
||||
computation: (itemVal): AccountDetailFormData => {
|
||||
return {
|
||||
code: (itemVal.code || '(Auto)').toString(),
|
||||
name: itemVal.name || '',
|
||||
type: itemVal.type ?? 0,
|
||||
isActive: itemVal.isActive ?? true,
|
||||
isReconcilable: itemVal.isReconcilable ?? false,
|
||||
costCentre: itemVal.costCentre?.id ?? '',
|
||||
};
|
||||
},
|
||||
});
|
||||
|
||||
accountForm = form(this.accountModel);
|
||||
|
||||
accountTypesResource = this.accountTypeService.list();
|
||||
costCentresResource = this.costCentreService.list();
|
||||
accountTypes = computed(() => this.accountTypesResource.value() || []);
|
||||
costCentres = computed(() => this.costCentresResource.value() || []);
|
||||
|
||||
constructor() {
|
||||
this.form = new FormGroup({
|
||||
code: new FormControl<string | number>({ value: 0, disabled: true }, { nonNullable: true }),
|
||||
name: new FormControl<string | null>(null),
|
||||
type: new FormControl<number>(0, { nonNullable: true }),
|
||||
isActive: new FormControl<boolean>(true, { nonNullable: true }),
|
||||
isReconcilable: new FormControl<boolean>(false, { nonNullable: true }),
|
||||
costCentre: new FormControl<string | null>(null),
|
||||
afterNextRender(() => {
|
||||
this.accountForm.name().focusBoundControl();
|
||||
});
|
||||
}
|
||||
|
||||
ngOnInit() {
|
||||
this.route.data.subscribe((value) => {
|
||||
const data = value as {
|
||||
item: Account;
|
||||
accountTypes: AccountType[];
|
||||
costCentres: CostCentre[];
|
||||
};
|
||||
|
||||
this.accountTypes = data.accountTypes;
|
||||
this.costCentres = data.costCentres;
|
||||
this.showItem(data.item);
|
||||
});
|
||||
}
|
||||
|
||||
showItem(item: Account) {
|
||||
this.item = item;
|
||||
this.form.setValue({
|
||||
code: this.item.code || '(Auto)',
|
||||
name: this.item.name || '',
|
||||
type: this.item.type,
|
||||
isActive: this.item.isActive,
|
||||
isReconcilable: this.item.isReconcilable,
|
||||
costCentre: this.item.costCentre.id ?? '',
|
||||
});
|
||||
}
|
||||
|
||||
ngAfterViewInit() {
|
||||
setTimeout(() => {
|
||||
this.nameElement.nativeElement.focus();
|
||||
}, 0);
|
||||
}
|
||||
|
||||
save() {
|
||||
this.ser.saveOrUpdate(this.getItem()).subscribe({
|
||||
next: () => {
|
||||
@@ -103,19 +94,19 @@ export class AccountDetailComponent implements OnInit, AfterViewInit {
|
||||
this.router.navigateByUrl('/accounts');
|
||||
},
|
||||
error: (error) => {
|
||||
this.snackBar.open(error, 'Danger');
|
||||
this.snackBar.open(error as string, 'Danger');
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
delete() {
|
||||
this.ser.delete(this.item.id as string).subscribe({
|
||||
this.ser.delete(this.item().id as string).subscribe({
|
||||
next: () => {
|
||||
this.snackBar.open('', 'Success');
|
||||
this.router.navigateByUrl('/accounts');
|
||||
},
|
||||
error: (error) => {
|
||||
this.snackBar.open(error, 'Danger');
|
||||
this.snackBar.open(error as string, 'Danger');
|
||||
},
|
||||
});
|
||||
}
|
||||
@@ -134,12 +125,13 @@ export class AccountDetailComponent implements OnInit, AfterViewInit {
|
||||
}
|
||||
|
||||
getItem(): Account {
|
||||
const formModel = this.form.value;
|
||||
this.item.name = formModel.name ?? '';
|
||||
this.item.type = formModel.type ?? 0;
|
||||
this.item.isActive = formModel.isActive ?? true;
|
||||
this.item.isReconcilable = formModel.isReconcilable ?? false;
|
||||
this.item.costCentre.id = formModel.costCentre ?? '';
|
||||
return this.item;
|
||||
const formModel = this.accountModel();
|
||||
const item = this.item();
|
||||
item.name = formModel.name ?? '';
|
||||
item.type = formModel.type ?? 0;
|
||||
item.isActive = formModel.isActive ?? true;
|
||||
item.isReconcilable = formModel.isReconcilable ?? false;
|
||||
item.costCentre.id = formModel.costCentre ?? '';
|
||||
return item;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,18 +0,0 @@
|
||||
import { TestBed } from '@angular/core/testing';
|
||||
import { ResolveFn } from '@angular/router';
|
||||
|
||||
import { Account } from '../core/account';
|
||||
import { accountListResolver } from './account-list.resolver';
|
||||
|
||||
describe('accountListResolver', () => {
|
||||
const executeResolver: ResolveFn<Account[]> = (...resolverParameters) =>
|
||||
TestBed.runInInjectionContext(() => accountListResolver(...resolverParameters));
|
||||
|
||||
beforeEach(() => {
|
||||
TestBed.configureTestingModule({});
|
||||
});
|
||||
|
||||
it('should be created', () => {
|
||||
expect(executeResolver).toBeTruthy();
|
||||
});
|
||||
});
|
||||
@@ -1,9 +0,0 @@
|
||||
import { inject } from '@angular/core';
|
||||
import { ResolveFn } from '@angular/router';
|
||||
|
||||
import { Account } from '../core/account';
|
||||
import { AccountService } from '../core/account.service';
|
||||
|
||||
export const accountListResolver: ResolveFn<Account[]> = () => {
|
||||
return inject(AccountService).list();
|
||||
};
|
||||
@@ -1,101 +0,0 @@
|
||||
import { DataSource } from '@angular/cdk/collections';
|
||||
import { EventEmitter } from '@angular/core';
|
||||
import { MatPaginator, PageEvent } from '@angular/material/paginator';
|
||||
import { MatSort, Sort } from '@angular/material/sort';
|
||||
import { merge, Observable, of as observableOf } from 'rxjs';
|
||||
import { map, tap } from 'rxjs/operators';
|
||||
|
||||
import { Account } from '../../core/account';
|
||||
|
||||
/** Simple sort comparator for example ID/Name columns (for client-side sorting). */
|
||||
const compare = (a: string | number | boolean, b: string | number | boolean, isAsc: boolean) =>
|
||||
(a < b ? -1 : 1) * (isAsc ? 1 : -1);
|
||||
export class AccountListDataSource extends DataSource<Account> {
|
||||
private filterValue = '';
|
||||
|
||||
constructor(
|
||||
public data: Account[],
|
||||
private filter: Observable<string>,
|
||||
private paginator?: MatPaginator,
|
||||
private sort?: MatSort,
|
||||
) {
|
||||
super();
|
||||
this.filter = filter.pipe(
|
||||
tap((x) => {
|
||||
this.filterValue = x;
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
connect(): Observable<Account[]> {
|
||||
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: Account[]) => {
|
||||
if (this.paginator) {
|
||||
this.paginator.length = x.length;
|
||||
}
|
||||
}),
|
||||
)
|
||||
.pipe(map((x: Account[]) => this.getPagedData(this.getSortedData(x))));
|
||||
}
|
||||
|
||||
disconnect() {}
|
||||
|
||||
private getFilteredData(data: Account[]): Account[] {
|
||||
return this.filterValue.split(' ').reduce(
|
||||
(p: Account[], c: string) =>
|
||||
p.filter((x) => {
|
||||
const accountString = `${x.name} ${x.type} ${x.costCentre}${
|
||||
x.isActive ? ' active' : ' inactive'
|
||||
}`.toLowerCase();
|
||||
return accountString.indexOf(c) !== -1;
|
||||
}),
|
||||
Object.assign([], data),
|
||||
);
|
||||
}
|
||||
|
||||
private getPagedData(data: Account[]) {
|
||||
if (this.paginator === undefined) {
|
||||
return data;
|
||||
}
|
||||
const startIndex = this.paginator.pageIndex * this.paginator.pageSize;
|
||||
return data.splice(startIndex, this.paginator.pageSize);
|
||||
}
|
||||
|
||||
private getSortedData(data: Account[]) {
|
||||
if (this.sort === undefined) {
|
||||
return data;
|
||||
}
|
||||
if (!this.sort.active || this.sort.direction === '') {
|
||||
return data;
|
||||
}
|
||||
|
||||
const sort = this.sort as MatSort;
|
||||
return data.sort((a, b) => {
|
||||
const isAsc = sort.direction === 'asc';
|
||||
switch (sort.active) {
|
||||
case 'name':
|
||||
return compare(a.name, b.name, isAsc);
|
||||
case 'type':
|
||||
return compare(a.typeName, b.typeName, isAsc);
|
||||
case 'isActive':
|
||||
return compare(a.isActive, b.isActive, isAsc);
|
||||
case 'isReconcilable':
|
||||
return compare(a.isReconcilable, b.isReconcilable, isAsc);
|
||||
case 'costCentre':
|
||||
return compare(a.costCentre.name, b.costCentre.name, isAsc);
|
||||
default:
|
||||
return 0;
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -6,61 +6,67 @@
|
||||
</a>
|
||||
</h2>
|
||||
|
||||
<form [formGroup]="form" class="flex-col">
|
||||
<form class="flex-col" [formRoot]="accountForm">
|
||||
<div class="row-container">
|
||||
<mat-form-field class="flex-auto">
|
||||
<mat-label>Filter</mat-label>
|
||||
<input type="text" matInput #filterElement formControlName="filter" autocomplete="off" />
|
||||
<input type="text" matInput #filterElement [formField]="accountForm.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 mat-sort-header>Name</mat-header-cell>
|
||||
<mat-cell *matCellDef="let row">
|
||||
<a [routerLink]="['/accounts', row.id]">
|
||||
@if (row.isStarred) {
|
||||
<mat-icon matSuffix class="gold">star </mat-icon>
|
||||
}
|
||||
{{ row.name }}
|
||||
</a>
|
||||
</mat-cell>
|
||||
</ng-container>
|
||||
@if (listResource.isLoading()) {
|
||||
<app-skeleton-loader type="table" [count]="1" />
|
||||
} @else if (listResource.error()) {
|
||||
<app-error-state [message]="'Failed to load data.'" (retryAction)="listResource.reload()" />
|
||||
} @else {
|
||||
<mat-table #table [dataSource]="dataSource()" matSort (matSortChange)="sortData($event)">
|
||||
<!-- Name Column -->
|
||||
<ng-container matColumnDef="name">
|
||||
<mat-header-cell *matHeaderCellDef mat-sort-header>Name</mat-header-cell>
|
||||
<mat-cell *matCellDef="let row">
|
||||
<a [routerLink]="['/accounts', row.id]">
|
||||
@if (row.isStarred) {
|
||||
<mat-icon matSuffix class="gold">star </mat-icon>
|
||||
}
|
||||
{{ row.name }}
|
||||
</a>
|
||||
</mat-cell>
|
||||
</ng-container>
|
||||
|
||||
<!-- Type Column -->
|
||||
<ng-container matColumnDef="type">
|
||||
<mat-header-cell *matHeaderCellDef mat-sort-header>Type</mat-header-cell>
|
||||
<mat-cell *matCellDef="let row">{{ row.typeName }}</mat-cell>
|
||||
</ng-container>
|
||||
<!-- Type Column -->
|
||||
<ng-container matColumnDef="type">
|
||||
<mat-header-cell *matHeaderCellDef mat-sort-header>Type</mat-header-cell>
|
||||
<mat-cell *matCellDef="let row">{{ row.typeName }}</mat-cell>
|
||||
</ng-container>
|
||||
|
||||
<!-- Is Active? Column -->
|
||||
<ng-container matColumnDef="isActive">
|
||||
<mat-header-cell *matHeaderCellDef mat-sort-header>Is Active?</mat-header-cell>
|
||||
<mat-cell *matCellDef="let row">{{ row.isActive }}</mat-cell>
|
||||
</ng-container>
|
||||
<!-- Is Active? Column -->
|
||||
<ng-container matColumnDef="isActive">
|
||||
<mat-header-cell *matHeaderCellDef mat-sort-header>Is Active?</mat-header-cell>
|
||||
<mat-cell *matCellDef="let row">{{ row.isActive }}</mat-cell>
|
||||
</ng-container>
|
||||
|
||||
<!-- Is Reconcilable? Column -->
|
||||
<ng-container matColumnDef="isReconcilable">
|
||||
<mat-header-cell *matHeaderCellDef mat-sort-header>Is Reconcilable?</mat-header-cell>
|
||||
<mat-cell *matCellDef="let row">{{ row.isReconcilable }}</mat-cell>
|
||||
</ng-container>
|
||||
<!-- Is Reconcilable? Column -->
|
||||
<ng-container matColumnDef="isReconcilable">
|
||||
<mat-header-cell *matHeaderCellDef mat-sort-header>Is Reconcilable?</mat-header-cell>
|
||||
<mat-cell *matCellDef="let row">{{ row.isReconcilable }}</mat-cell>
|
||||
</ng-container>
|
||||
|
||||
<!-- Cost Centre Column -->
|
||||
<ng-container matColumnDef="costCentre">
|
||||
<mat-header-cell *matHeaderCellDef mat-sort-header>Cost Centre</mat-header-cell>
|
||||
<mat-cell *matCellDef="let row">{{ row.costCentre.name }}</mat-cell>
|
||||
</ng-container>
|
||||
<!-- Cost Centre Column -->
|
||||
<ng-container matColumnDef="costCentre">
|
||||
<mat-header-cell *matHeaderCellDef mat-sort-header>Cost Centre</mat-header-cell>
|
||||
<mat-cell *matCellDef="let row">{{ row.costCentre.name }}</mat-cell>
|
||||
</ng-container>
|
||||
|
||||
<mat-header-row *matHeaderRowDef="displayedColumns"></mat-header-row>
|
||||
<mat-row *matRowDef="let row; columns: displayedColumns"></mat-row>
|
||||
</mat-table>
|
||||
<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.length"
|
||||
[pageIndex]="0"
|
||||
[pageSize]="50"
|
||||
[pageSizeOptions]="[25, 50, 100, 250]"
|
||||
>
|
||||
</mat-paginator>
|
||||
<mat-paginator
|
||||
(page)="handlePageEvent($event)"
|
||||
[length]="filteredList().length"
|
||||
[pageIndex]="pageIndex()"
|
||||
[pageSize]="pageSize()"
|
||||
[pageSizeOptions]="[25, 50, 100, 250]"
|
||||
>
|
||||
</mat-paginator>
|
||||
}
|
||||
|
||||
@@ -1,77 +1,174 @@
|
||||
import { AfterViewInit, Component, ElementRef, inject, OnInit, ViewChild } from '@angular/core';
|
||||
import { FormControl, FormGroup, ReactiveFormsModule } from '@angular/forms';
|
||||
import {
|
||||
Component,
|
||||
inject,
|
||||
signal,
|
||||
effect,
|
||||
computed,
|
||||
debounced,
|
||||
input,
|
||||
linkedSignal,
|
||||
afterNextRender,
|
||||
} from '@angular/core';
|
||||
import { form, FormField, FormRoot } from '@angular/forms/signals';
|
||||
import { MatButtonModule } from '@angular/material/button';
|
||||
import { MatFormFieldModule } from '@angular/material/form-field';
|
||||
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 { MatPaginatorModule, PageEvent } from '@angular/material/paginator';
|
||||
import { MatSortModule, Sort } from '@angular/material/sort';
|
||||
import { MatTableModule } from '@angular/material/table';
|
||||
import { ActivatedRoute, RouterModule } from '@angular/router';
|
||||
import { Observable } from 'rxjs';
|
||||
import { debounceTime, distinctUntilChanged } from 'rxjs/operators';
|
||||
import { ActivatedRoute, Router, RouterLink } from '@angular/router';
|
||||
|
||||
import { Account } from '../../core/account';
|
||||
import { AccountType } from '../../core/account-type';
|
||||
import { AccountListDataSource } from './account-list-datasource';
|
||||
import { AccountService } from '../../core/account.service';
|
||||
import { ErrorStateComponent } from '../../shared/error-state/error-state.component';
|
||||
import { SkeletonLoaderComponent } from '../../shared/skeleton-loader/skeleton-loader.component';
|
||||
import { AccountTypeService } from '../account-type.service';
|
||||
|
||||
export interface AccountListFormData {
|
||||
filter: string;
|
||||
}
|
||||
|
||||
@Component({
|
||||
selector: 'app-account-list',
|
||||
templateUrl: './account-list.component.html',
|
||||
styleUrls: ['./account-list.component.css'],
|
||||
imports: [
|
||||
RouterModule,
|
||||
RouterLink,
|
||||
MatButtonModule,
|
||||
MatIconModule,
|
||||
ReactiveFormsModule,
|
||||
FormField,
|
||||
FormRoot,
|
||||
MatFormFieldModule,
|
||||
MatInputModule,
|
||||
MatTableModule,
|
||||
MatSortModule,
|
||||
MatPaginatorModule,
|
||||
|
||||
SkeletonLoaderComponent,
|
||||
ErrorStateComponent,
|
||||
],
|
||||
})
|
||||
export class AccountListComponent implements OnInit, AfterViewInit {
|
||||
export class AccountListComponent {
|
||||
private route = inject(ActivatedRoute);
|
||||
private router = inject(Router);
|
||||
private ser = inject(AccountService);
|
||||
private accountTypeSer = inject(AccountTypeService);
|
||||
pageSize = signal(50);
|
||||
pageIndex = signal(0);
|
||||
sortActive = signal('');
|
||||
sortDirection = signal('');
|
||||
q = input('', { transform: (v: string | null | undefined) => v ?? '' });
|
||||
formModel = linkedSignal({
|
||||
source: this.q,
|
||||
computation: (qValue): AccountListFormData => ({ filter: qValue }),
|
||||
});
|
||||
|
||||
@ViewChild('filterElement', { static: true }) filterElement!: ElementRef<HTMLInputElement>;
|
||||
@ViewChild(MatPaginator, { static: true }) paginator!: MatPaginator;
|
||||
@ViewChild(MatSort, { static: true }) sort!: MatSort;
|
||||
dataSource: AccountListDataSource;
|
||||
filter: Observable<string>;
|
||||
form: FormGroup<{
|
||||
filter: FormControl<string>;
|
||||
}>;
|
||||
accountForm = form(this.formModel);
|
||||
|
||||
list: Account[] = [];
|
||||
accountTypes: AccountType[] = [];
|
||||
filterSignal = computed(() => this.formModel().filter);
|
||||
|
||||
debouncedFilter = debounced(this.filterSignal, 150);
|
||||
|
||||
listResource = this.ser.list();
|
||||
accountTypesResource = this.accountTypeSer.list();
|
||||
|
||||
accountTypes = computed(() => this.accountTypesResource.value() ?? []);
|
||||
list = computed(() => this.listResource.value() ?? []);
|
||||
listWithTypes = computed(() => {
|
||||
const data = this.list();
|
||||
const types = this.accountTypes();
|
||||
|
||||
return data.map(
|
||||
(account) =>
|
||||
new Account({
|
||||
...account,
|
||||
typeName: types.find((t) => t.id === account.type)?.name ?? account.type.toString(),
|
||||
}),
|
||||
);
|
||||
});
|
||||
|
||||
filteredList = computed(() => {
|
||||
const q = (this.debouncedFilter.value() ?? '').trim().toLowerCase();
|
||||
const data = this.listWithTypes();
|
||||
if (!q) {
|
||||
return data;
|
||||
}
|
||||
return q.split(' ').reduce(
|
||||
(p: Account[], c: string) =>
|
||||
p.filter((x) => {
|
||||
const accountString = `${x.name} ${x.type} ${x.costCentre}${
|
||||
x.isActive ? ' active' : ' inactive'
|
||||
}`.toLowerCase();
|
||||
return accountString.indexOf(c) !== -1;
|
||||
}),
|
||||
[...data],
|
||||
);
|
||||
});
|
||||
|
||||
sortedList = computed(() => {
|
||||
const data = this.filteredList();
|
||||
const active = this.sortActive();
|
||||
const direction = this.sortDirection();
|
||||
|
||||
if (!active || direction === '') {
|
||||
return data;
|
||||
}
|
||||
|
||||
return [...data].sort((a, b) => {
|
||||
const isAsc = direction === 'asc';
|
||||
switch (active) {
|
||||
case 'name':
|
||||
return compare(a.name, b.name, isAsc);
|
||||
case 'type':
|
||||
return compare(a.typeName, b.typeName, isAsc);
|
||||
case 'isActive':
|
||||
return compare(a.isActive, b.isActive, isAsc);
|
||||
case 'isReconcilable':
|
||||
return compare(a.isReconcilable, b.isReconcilable, isAsc);
|
||||
case 'costCentre':
|
||||
return compare(a.costCentre.name, b.costCentre.name, isAsc);
|
||||
default:
|
||||
return 0;
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
dataSource = computed(() => {
|
||||
const data = this.sortedList();
|
||||
const pageIndex = this.pageIndex();
|
||||
const pageSize = this.pageSize();
|
||||
return data.slice(pageIndex * pageSize, (pageIndex + 1) * pageSize);
|
||||
});
|
||||
|
||||
/** Columns displayed in the table. Columns IDs can be added, removed, or reordered. */
|
||||
displayedColumns = ['name', 'type', 'isActive', 'isReconcilable', 'costCentre'];
|
||||
|
||||
constructor() {
|
||||
this.form = new FormGroup({
|
||||
filter: new FormControl<string>('', { nonNullable: true }),
|
||||
afterNextRender(() => {
|
||||
this.accountForm().focusBoundControl();
|
||||
});
|
||||
effect(() => {
|
||||
const q = this.debouncedFilter.value() ?? '';
|
||||
this.router.navigate([], {
|
||||
relativeTo: this.route,
|
||||
queryParams: { q: q || null },
|
||||
queryParamsHandling: 'merge',
|
||||
replaceUrl: true,
|
||||
});
|
||||
});
|
||||
// Listen to Filter Change
|
||||
this.filter = this.form.controls.filter.valueChanges.pipe(debounceTime(150), distinctUntilChanged());
|
||||
|
||||
this.dataSource = new AccountListDataSource(this.list, this.filter, this.paginator, this.sort);
|
||||
}
|
||||
|
||||
ngOnInit() {
|
||||
this.route.data.subscribe((value) => {
|
||||
const data = value as { list: Account[]; accountTypes: AccountType[] };
|
||||
this.accountTypes = data.accountTypes;
|
||||
data.list.forEach((x) => (x.typeName = this.accountTypes.find((y) => y.id === x.type)?.name ?? ''));
|
||||
this.list = data.list;
|
||||
});
|
||||
this.dataSource = new AccountListDataSource(this.list, this.filter, this.paginator, this.sort);
|
||||
handlePageEvent(e: PageEvent) {
|
||||
this.pageSize.set(e.pageSize);
|
||||
this.pageIndex.set(e.pageIndex);
|
||||
}
|
||||
|
||||
ngAfterViewInit() {
|
||||
setTimeout(() => {
|
||||
this.filterElement.nativeElement.focus();
|
||||
}, 0);
|
||||
sortData(sort: Sort) {
|
||||
this.sortActive.set(sort.active);
|
||||
this.sortDirection.set(sort.direction);
|
||||
}
|
||||
}
|
||||
|
||||
const compare = (a: string | number | boolean, b: string | number | boolean, isAsc: boolean) =>
|
||||
(a < b ? -1 : 1) * (isAsc ? 1 : -1);
|
||||
|
||||
@@ -1,18 +0,0 @@
|
||||
import { TestBed } from '@angular/core/testing';
|
||||
import { ResolveFn } from '@angular/router';
|
||||
|
||||
import { AccountType } from '../core/account-type';
|
||||
import { accountTypeResolver } from './account-type.resolver';
|
||||
|
||||
describe('accountTypeResolver', () => {
|
||||
const executeResolver: ResolveFn<AccountType[]> = (...resolverParameters) =>
|
||||
TestBed.runInInjectionContext(() => accountTypeResolver(...resolverParameters));
|
||||
|
||||
beforeEach(() => {
|
||||
TestBed.configureTestingModule({});
|
||||
});
|
||||
|
||||
it('should be created', () => {
|
||||
expect(executeResolver).toBeTruthy();
|
||||
});
|
||||
});
|
||||
@@ -1,9 +0,0 @@
|
||||
import { inject } from '@angular/core';
|
||||
import { ResolveFn } from '@angular/router';
|
||||
|
||||
import { AccountType } from '../core/account-type';
|
||||
import { AccountTypeService } from './account-type.service';
|
||||
|
||||
export const accountTypeResolver: ResolveFn<AccountType[]> = () => {
|
||||
return inject(AccountTypeService).list();
|
||||
};
|
||||
@@ -1,14 +1,10 @@
|
||||
import { HttpClient } from '@angular/common/http';
|
||||
import { HttpClient, httpResource } from '@angular/common/http';
|
||||
import { inject, Injectable } from '@angular/core';
|
||||
import { Observable } from 'rxjs/internal/Observable';
|
||||
import { catchError } from 'rxjs/operators';
|
||||
|
||||
import { AccountType } from '../core/account-type';
|
||||
import { ErrorLoggerService } from '../core/error-logger.service';
|
||||
|
||||
const url = '/api/account-types';
|
||||
const serviceName = 'AccountTypeService';
|
||||
|
||||
@Injectable({
|
||||
providedIn: 'root',
|
||||
})
|
||||
@@ -16,9 +12,7 @@ export class AccountTypeService {
|
||||
private http = inject(HttpClient);
|
||||
private log = inject(ErrorLoggerService);
|
||||
|
||||
list(): Observable<AccountType[]> {
|
||||
return this.http.get<AccountType[]>(url).pipe(catchError(this.log.handleError(serviceName, 'list'))) as Observable<
|
||||
AccountType[]
|
||||
>;
|
||||
list() {
|
||||
return httpResource<AccountType[]>(() => url);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,18 +0,0 @@
|
||||
import { TestBed } from '@angular/core/testing';
|
||||
import { ResolveFn } from '@angular/router';
|
||||
|
||||
import { Account } from '../core/account';
|
||||
import { accountResolver } from './account.resolver';
|
||||
|
||||
describe('accountResolver', () => {
|
||||
const executeResolver: ResolveFn<Account> = (...resolverParameters) =>
|
||||
TestBed.runInInjectionContext(() => accountResolver(...resolverParameters));
|
||||
|
||||
beforeEach(() => {
|
||||
TestBed.configureTestingModule({});
|
||||
});
|
||||
|
||||
it('should be created', () => {
|
||||
expect(executeResolver).toBeTruthy();
|
||||
});
|
||||
});
|
||||
@@ -1,10 +0,0 @@
|
||||
import { inject } from '@angular/core';
|
||||
import { ResolveFn } from '@angular/router';
|
||||
|
||||
import { Account } from '../core/account';
|
||||
import { AccountService } from '../core/account.service';
|
||||
|
||||
export const accountResolver: ResolveFn<Account> = (route) => {
|
||||
const id = route.paramMap.get('id');
|
||||
return inject(AccountService).get(id);
|
||||
};
|
||||
@@ -1,12 +1,8 @@
|
||||
import { Routes } from '@angular/router';
|
||||
|
||||
import { authGuard } from '../auth/auth-guard.service';
|
||||
import { costCentreListResolver } from '../cost-centre/cost-centre-list.resolver';
|
||||
import { AccountDetailComponent } from './account-detail/account-detail.component';
|
||||
import { accountListResolver } from './account-list.resolver';
|
||||
import { AccountListComponent } from './account-list/account-list.component';
|
||||
import { accountTypeResolver } from './account-type.resolver';
|
||||
import { accountResolver } from './account.resolver';
|
||||
|
||||
export const routes: Routes = [
|
||||
{
|
||||
@@ -16,10 +12,6 @@ export const routes: Routes = [
|
||||
data: {
|
||||
permission: 'Accounts',
|
||||
},
|
||||
resolve: {
|
||||
list: accountListResolver,
|
||||
accountTypes: accountTypeResolver,
|
||||
},
|
||||
},
|
||||
{
|
||||
path: 'new',
|
||||
@@ -28,11 +20,6 @@ export const routes: Routes = [
|
||||
data: {
|
||||
permission: 'Accounts',
|
||||
},
|
||||
resolve: {
|
||||
item: accountResolver,
|
||||
accountTypes: accountTypeResolver,
|
||||
costCentres: costCentreListResolver,
|
||||
},
|
||||
},
|
||||
{
|
||||
path: ':id',
|
||||
@@ -41,10 +28,5 @@ export const routes: Routes = [
|
||||
data: {
|
||||
permission: 'Accounts',
|
||||
},
|
||||
resolve: {
|
||||
item: accountResolver,
|
||||
accountTypes: accountTypeResolver,
|
||||
costCentres: costCentreListResolver,
|
||||
},
|
||||
},
|
||||
];
|
||||
|
||||
Reference in New Issue
Block a user