Consolidated commit message to v22 signals
This commit is contained in:
1 parent
6403d25d3e
commit
df289b20b8
443 files changed
+16058
-17332
No files matched your search
+25
-24
@@ -14,17 +14,17 @@
|
||||
},
|
||||
"private": true,
|
||||
"dependencies": {
|
||||
"@angular/animations": "^21.1.5",
|
||||
"@angular/cdk": "^21.1.5",
|
||||
"@angular/common": "^21.1.5",
|
||||
"@angular/compiler": "^21.1.5",
|
||||
"@angular/core": "^21.1.5",
|
||||
"@angular/forms": "^21.1.5",
|
||||
"@angular/material": "^21.1.5",
|
||||
"@angular/material-moment-adapter": "^21.1.5",
|
||||
"@angular/platform-browser": "^21.1.5",
|
||||
"@angular/platform-browser-dynamic": "^21.1.5",
|
||||
"@angular/router": "^21.1.5",
|
||||
"@angular/animations": "^22.1.2",
|
||||
"@angular/cdk": "^22.1.2",
|
||||
"@angular/common": "^22.1.2",
|
||||
"@angular/compiler": "^22.1.2",
|
||||
"@angular/core": "^22.1.2",
|
||||
"@angular/forms": "^22.1.2",
|
||||
"@angular/material": "^22.1.2",
|
||||
"@angular/material-moment-adapter": "^22.1.2",
|
||||
"@angular/platform-browser": "^22.1.2",
|
||||
"@angular/platform-browser-dynamic": "^22.1.2",
|
||||
"@angular/router": "^22.1.2",
|
||||
"html2canvas": "^1.4.1",
|
||||
"jspdf": "^4.2.0",
|
||||
"mathjs": "^15.1.1",
|
||||
@@ -33,22 +33,23 @@
|
||||
"tslib": "^2.6.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@angular/build": "^21.1.4",
|
||||
"@angular/cli": "^21.1.4",
|
||||
"@angular/compiler-cli": "^21.1.5",
|
||||
"@angular/language-service": "^21.1.5",
|
||||
"angular-eslint": "^21.2.0",
|
||||
"eslint": "^9.28.0",
|
||||
"@angular/build": "^22.1.4",
|
||||
"@angular/cli": "^22.1.4",
|
||||
"@angular/compiler-cli": "^22.1.2",
|
||||
"@angular/language-service": "^22.1.2",
|
||||
"@eslint/js": "10.0.1",
|
||||
"angular-eslint": "^22.1.0",
|
||||
"eslint": "^10.8.1",
|
||||
"eslint-config-prettier": "^10.1.5",
|
||||
"eslint-plugin-import": "^2.31.0",
|
||||
"eslint-plugin-perfectionist": "^4.15.0",
|
||||
"eslint-plugin-simple-import-sort": "^12.1.1",
|
||||
"eslint-plugin-perfectionist": "^5.10.1",
|
||||
"eslint-plugin-simple-import-sort": "^14.0.0",
|
||||
"eslint-plugin-unused-imports": "^4.1.4",
|
||||
"husky": "^9.0.11",
|
||||
"lint-staged": "^16.2.7",
|
||||
"prettier": "^3.8.1",
|
||||
"typescript": "~5.9.3",
|
||||
"typescript-eslint": "^8.33.1"
|
||||
"lint-staged": "^17.3.0",
|
||||
"prettier": "^3.9.6",
|
||||
"typescript": "~6.0.3",
|
||||
"typescript-eslint": "^8.67.0"
|
||||
},
|
||||
"husky": {
|
||||
"hooks": {
|
||||
@@ -63,4 +64,4 @@
|
||||
"npx eslint --cache --fix"
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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,
|
||||
},
|
||||
},
|
||||
];
|
||||
@@ -1,60 +1,44 @@
|
||||
import { LayoutModule } from '@angular/cdk/layout';
|
||||
import { provideHttpClient, withInterceptors, withFetch } from '@angular/common/http';
|
||||
import { ApplicationConfig, importProvidersFrom, LOCALE_ID, provideZonelessChangeDetection } from '@angular/core';
|
||||
import { ReactiveFormsModule } from '@angular/forms';
|
||||
import { provideHttpClient, withInterceptors } from '@angular/common/http';
|
||||
import {
|
||||
ApplicationConfig,
|
||||
importProvidersFrom,
|
||||
isDevMode,
|
||||
LOCALE_ID,
|
||||
provideZonelessChangeDetection,
|
||||
} from '@angular/core';
|
||||
import { MomentDateAdapter } from '@angular/material-moment-adapter';
|
||||
import { MatButtonModule } from '@angular/material/button';
|
||||
import { DateAdapter, MAT_DATE_FORMATS, MAT_DATE_LOCALE } from '@angular/material/core';
|
||||
import { MatDividerModule } from '@angular/material/divider';
|
||||
import { MatIconModule } from '@angular/material/icon';
|
||||
import { MatInputModule } from '@angular/material/input';
|
||||
import { MatListModule } from '@angular/material/list';
|
||||
import { MatMenuModule } from '@angular/material/menu';
|
||||
import { MatPaginatorModule } from '@angular/material/paginator';
|
||||
import { MatProgressSpinnerModule } from '@angular/material/progress-spinner';
|
||||
import { MatSidenavModule } from '@angular/material/sidenav';
|
||||
import { MAT_SNACK_BAR_DEFAULT_OPTIONS, MatSnackBarModule } from '@angular/material/snack-bar';
|
||||
import { MatSortModule } from '@angular/material/sort';
|
||||
import { MatTableModule } from '@angular/material/table';
|
||||
import { MatToolbarModule } from '@angular/material/toolbar';
|
||||
import { MAT_SNACK_BAR_DEFAULT_OPTIONS } from '@angular/material/snack-bar';
|
||||
import { BrowserModule } from '@angular/platform-browser';
|
||||
import { provideRouter, withRouterConfig } from '@angular/router';
|
||||
import { provideRouter, withComponentInputBinding, withRouterConfig } from '@angular/router';
|
||||
|
||||
import { dateFormat } from './app.environment';
|
||||
import { routes } from './app.routes';
|
||||
import { AuthService } from './auth/auth.service';
|
||||
import { authInterceptor } from './core/auth.interceptor';
|
||||
import { delayInterceptor } from './core/delay-interceptor';
|
||||
import { jwtInterceptor } from './core/jwt.interceptor';
|
||||
import { refreshInterceptor } from './core/refresh.interceptor';
|
||||
|
||||
export const appConfig: ApplicationConfig = {
|
||||
providers: [
|
||||
importProvidersFrom(
|
||||
BrowserModule,
|
||||
LayoutModule,
|
||||
MatButtonModule,
|
||||
MatDividerModule,
|
||||
MatIconModule,
|
||||
MatInputModule,
|
||||
MatSidenavModule,
|
||||
MatToolbarModule,
|
||||
MatListModule,
|
||||
MatMenuModule,
|
||||
MatProgressSpinnerModule,
|
||||
MatSnackBarModule,
|
||||
MatTableModule,
|
||||
MatPaginatorModule,
|
||||
MatSortModule,
|
||||
ReactiveFormsModule,
|
||||
),
|
||||
importProvidersFrom(BrowserModule),
|
||||
{ provide: LOCALE_ID, useValue: 'en-IN' },
|
||||
AuthService,
|
||||
provideHttpClient(withFetch(), withInterceptors([refreshInterceptor, jwtInterceptor, authInterceptor])),
|
||||
provideHttpClient(
|
||||
withInterceptors([
|
||||
authInterceptor,
|
||||
jwtInterceptor,
|
||||
refreshInterceptor,
|
||||
...(isDevMode() ? [delayInterceptor] : []),
|
||||
]),
|
||||
),
|
||||
provideRouter(
|
||||
routes,
|
||||
withRouterConfig({
|
||||
onSameUrlNavigation: 'reload',
|
||||
}),
|
||||
withComponentInputBinding(),
|
||||
),
|
||||
{ provide: DateAdapter, useClass: MomentDateAdapter, deps: [MAT_DATE_LOCALE] },
|
||||
{ provide: MAT_DATE_FORMATS, useValue: dateFormat },
|
||||
|
||||
@@ -1,16 +0,0 @@
|
||||
import { DataSource } from '@angular/cdk/collections';
|
||||
import { Observable } from 'rxjs';
|
||||
|
||||
import { AttendanceItem } from './attendance-item';
|
||||
|
||||
export class AttendanceDataSource extends DataSource<AttendanceItem> {
|
||||
constructor(private data: Observable<AttendanceItem[]>) {
|
||||
super();
|
||||
}
|
||||
|
||||
connect(): Observable<AttendanceItem[]> {
|
||||
return this.data;
|
||||
}
|
||||
|
||||
disconnect() {}
|
||||
}
|
||||
@@ -1,9 +0,0 @@
|
||||
import { inject } from '@angular/core';
|
||||
import { ResolveFn } from '@angular/router';
|
||||
|
||||
import { AttendanceType } from './attendance-type';
|
||||
import { AttendanceTypeService } from './attendance-type.service';
|
||||
|
||||
export const attendanceTypeResolver: ResolveFn<AttendanceType[]> = () => {
|
||||
return inject(AttendanceTypeService).list();
|
||||
};
|
||||
@@ -1,22 +1,16 @@
|
||||
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 { ErrorLoggerService } from '../core/error-logger.service';
|
||||
import { AttendanceType } from './attendance-type';
|
||||
|
||||
const url = '/api/attendance-types';
|
||||
const serviceName = 'AttendanceTypeService';
|
||||
|
||||
@Injectable({ providedIn: 'root' })
|
||||
export class AttendanceTypeService {
|
||||
private http = inject(HttpClient);
|
||||
private log = inject(ErrorLoggerService);
|
||||
|
||||
list(): Observable<AttendanceType[]> {
|
||||
return this.http
|
||||
.get<AttendanceType[]>(url)
|
||||
.pipe(catchError(this.log.handleError(serviceName, 'list'))) as Observable<AttendanceType[]>;
|
||||
list() {
|
||||
return httpResource<AttendanceType[]>(() => url);
|
||||
}
|
||||
}
|
||||
@@ -1,6 +1,6 @@
|
||||
<h2>Attendance</h2>
|
||||
|
||||
<form [formGroup]="form" class="flex-col">
|
||||
<form class="flex-col" [formRoot]="form">
|
||||
<div class="row-container">
|
||||
<mat-form-field class="flex-auto basis-4-5">
|
||||
<mat-label>Date</mat-label>
|
||||
@@ -9,7 +9,7 @@
|
||||
#dateElement
|
||||
[matDatepicker]="date"
|
||||
(focus)="dateElement.select()"
|
||||
formControlName="date"
|
||||
[formField]="form.date"
|
||||
autocomplete="off"
|
||||
/>
|
||||
<mat-datepicker-toggle matSuffix [for]="date"></mat-datepicker-toggle>
|
||||
@@ -17,73 +17,81 @@
|
||||
</mat-form-field>
|
||||
<button mat-raised-button color="primary" (click)="show()" class="flex-auto basis-1-5">Show</button>
|
||||
</div>
|
||||
<mat-table #table [dataSource]="dataSource" formArrayName="attendances">
|
||||
<!-- Code Column -->
|
||||
<ng-container matColumnDef="code">
|
||||
<mat-header-cell *matHeaderCellDef class="right">Code</mat-header-cell>
|
||||
<mat-cell *matCellDef="let row" class="right">{{ row.code }}</mat-cell>
|
||||
</ng-container>
|
||||
@if (infoResource.isLoading()) {
|
||||
<app-skeleton-loader type="table" [count]="1" />
|
||||
} @else if (infoResource.error()) {
|
||||
<app-error-state [message]="'Failed to load data.'" (retryAction)="infoResource.reload()" />
|
||||
} @else {
|
||||
<mat-table #table [dataSource]="info().body">
|
||||
<!-- Code Column -->
|
||||
<ng-container matColumnDef="code">
|
||||
<mat-header-cell *matHeaderCellDef class="right">Code</mat-header-cell>
|
||||
<mat-cell *matCellDef="let row" class="right">{{ row.code }}</mat-cell>
|
||||
</ng-container>
|
||||
|
||||
<!-- Name Column -->
|
||||
<ng-container matColumnDef="name">
|
||||
<mat-header-cell *matHeaderCellDef>Name</mat-header-cell>
|
||||
<mat-cell *matCellDef="let row">{{ row.name }}</mat-cell>
|
||||
</ng-container>
|
||||
<!-- Name Column -->
|
||||
<ng-container matColumnDef="name">
|
||||
<mat-header-cell *matHeaderCellDef>Name</mat-header-cell>
|
||||
<mat-cell *matCellDef="let row">{{ row.name }}</mat-cell>
|
||||
</ng-container>
|
||||
|
||||
<!-- Designation Column -->
|
||||
<ng-container matColumnDef="designation">
|
||||
<mat-header-cell *matHeaderCellDef>Designation</mat-header-cell>
|
||||
<mat-cell *matCellDef="let row">{{ row.designation }}</mat-cell>
|
||||
</ng-container>
|
||||
<!-- Designation Column -->
|
||||
<ng-container matColumnDef="designation">
|
||||
<mat-header-cell *matHeaderCellDef>Designation</mat-header-cell>
|
||||
<mat-cell *matCellDef="let row">{{ row.designation }}</mat-cell>
|
||||
</ng-container>
|
||||
|
||||
<!-- Department Column -->
|
||||
<ng-container matColumnDef="department">
|
||||
<mat-header-cell *matHeaderCellDef>Department</mat-header-cell>
|
||||
<mat-cell *matCellDef="let row">{{ row.department }}</mat-cell>
|
||||
</ng-container>
|
||||
<!-- Department Column -->
|
||||
<ng-container matColumnDef="department">
|
||||
<mat-header-cell *matHeaderCellDef>Department</mat-header-cell>
|
||||
<mat-cell *matCellDef="let row">{{ row.department }}</mat-cell>
|
||||
</ng-container>
|
||||
|
||||
<!-- Status Column -->
|
||||
<ng-container matColumnDef="status">
|
||||
<mat-header-cell *matHeaderCellDef>Status</mat-header-cell>
|
||||
<mat-cell *matCellDef="let row; let i = index" [formGroupName]="i" class="flex-auto">
|
||||
<mat-select formControlName="attendanceType" name="attendanceType">
|
||||
@for (at of attendanceTypes; track at) {
|
||||
<mat-option [value]="at.id">
|
||||
{{ at.name }}
|
||||
</mat-option>
|
||||
<!-- Status Column -->
|
||||
<ng-container matColumnDef="status">
|
||||
<mat-header-cell *matHeaderCellDef>Status</mat-header-cell>
|
||||
<mat-cell *matCellDef="let row; let i = index" class="flex-auto">
|
||||
@if (form.attendances[i]) {
|
||||
<mat-select [formField]="form.attendances[i].attendanceType">
|
||||
@for (at of attendanceTypes(); track at) {
|
||||
<mat-option [value]="at.id">
|
||||
{{ at.name }}
|
||||
</mat-option>
|
||||
}
|
||||
</mat-select>
|
||||
}
|
||||
</mat-select>
|
||||
</mat-cell>
|
||||
</ng-container>
|
||||
</mat-cell>
|
||||
</ng-container>
|
||||
|
||||
<!-- Prints Column -->
|
||||
<ng-container matColumnDef="prints">
|
||||
<mat-header-cell *matHeaderCellDef>Prints</mat-header-cell>
|
||||
<mat-cell *matCellDef="let row; let i = index" class="no-bg">
|
||||
{{ row.prints }}
|
||||
@if (!form.controls.attendances.controls[i].controls.attendanceType.pristine) {
|
||||
<mat-icon class="no-bg">new_releases</mat-icon>
|
||||
}
|
||||
<mat-chip-set class="no-bg">
|
||||
@if (row.hoursWorked.length) {
|
||||
<mat-chip-option
|
||||
class="no-bg"
|
||||
selected
|
||||
selectable="false"
|
||||
[color]="row.fullDay === true ? 'primary' : 'warn'"
|
||||
>
|
||||
{{ row.hoursWorked }}
|
||||
</mat-chip-option>
|
||||
<!-- Prints Column -->
|
||||
<ng-container matColumnDef="prints">
|
||||
<mat-header-cell *matHeaderCellDef>Prints</mat-header-cell>
|
||||
<mat-cell *matCellDef="let row; let i = index" class="no-bg">
|
||||
{{ row.prints }}
|
||||
@if (form.attendances[i] && form.attendances[i].attendanceType !== null) {
|
||||
<mat-icon class="no-bg">new_releases</mat-icon>
|
||||
}
|
||||
</mat-chip-set>
|
||||
</mat-cell>
|
||||
</ng-container>
|
||||
<mat-chip-set class="no-bg">
|
||||
@if (row.hoursWorked.length) {
|
||||
<mat-chip-option
|
||||
class="no-bg"
|
||||
selected
|
||||
selectable="false"
|
||||
[color]="row.fullDay === true ? 'primary' : 'warn'"
|
||||
>
|
||||
{{ row.hoursWorked }}
|
||||
</mat-chip-option>
|
||||
}
|
||||
</mat-chip-set>
|
||||
</mat-cell>
|
||||
</ng-container>
|
||||
|
||||
<mat-header-row *matHeaderRowDef="displayedColumns"></mat-header-row>
|
||||
<mat-row *matRowDef="let row; let i = index; columns: displayedColumns" [class]="getClass(i)"></mat-row>
|
||||
</mat-table>
|
||||
<mat-header-row *matHeaderRowDef="displayedColumns"></mat-header-row>
|
||||
<mat-row *matRowDef="let row; let i = index; columns: displayedColumns" [class]="getClass(i)"></mat-row>
|
||||
</mat-table>
|
||||
}
|
||||
</form>
|
||||
|
||||
<div class="row-container">
|
||||
<button mat-raised-button color="primary" (click)="save()" [disabled]="form.pristine">Save</button>
|
||||
<button mat-raised-button color="primary" (click)="save()">Save</button>
|
||||
</div>
|
||||
@@ -1,5 +1,5 @@
|
||||
import { Component, ElementRef, HostListener, inject, OnInit, ViewChild } from '@angular/core';
|
||||
import { FormArray, FormControl, FormGroup, ReactiveFormsModule } from '@angular/forms';
|
||||
import { Component, HostListener, inject, input, computed, linkedSignal, afterNextRender } from '@angular/core';
|
||||
import { form as createForm, FormField, FormRoot } from '@angular/forms/signals';
|
||||
import { MatButtonModule } from '@angular/material/button';
|
||||
import { MatChipsModule } from '@angular/material/chips';
|
||||
import { MatOptionModule } from '@angular/material/core';
|
||||
@@ -11,23 +11,28 @@ import { MatInputModule } from '@angular/material/input';
|
||||
import { MatSelectModule } from '@angular/material/select';
|
||||
import { MatSnackBar } from '@angular/material/snack-bar';
|
||||
import { MatTableModule } from '@angular/material/table';
|
||||
import { ActivatedRoute, Router } from '@angular/router';
|
||||
import { Router } from '@angular/router';
|
||||
import moment from 'moment';
|
||||
import { BehaviorSubject } from 'rxjs';
|
||||
|
||||
import { AuthService } from '../auth/auth.service';
|
||||
import { ErrorStateComponent } from '../shared/error-state/error-state.component';
|
||||
import { SkeletonLoaderComponent } from '../shared/skeleton-loader/skeleton-loader.component';
|
||||
import { Attendance } from './attendance';
|
||||
import { AttendanceDataSource } from './attendance-datasource';
|
||||
import { AttendanceItem } from './attendance-item';
|
||||
import { AttendanceType } from './attendance-type';
|
||||
import { AttendanceTypeService } from './attendance-type.service';
|
||||
import { AttendanceService } from './attendance.service';
|
||||
|
||||
export interface AttendanceFormData {
|
||||
date: moment.Moment;
|
||||
attendances: { attendanceType: number | null }[];
|
||||
}
|
||||
|
||||
@Component({
|
||||
selector: 'app-attendance',
|
||||
templateUrl: './attendance.component.html',
|
||||
styleUrls: ['./attendance.component.css'],
|
||||
imports: [
|
||||
ReactiveFormsModule,
|
||||
FormField,
|
||||
FormRoot,
|
||||
MatFormFieldModule,
|
||||
MatInputModule,
|
||||
MatDatepickerModule,
|
||||
@@ -37,76 +42,62 @@ import { AttendanceService } from './attendance.service';
|
||||
MatOptionModule,
|
||||
MatIconModule,
|
||||
MatChipsModule,
|
||||
|
||||
SkeletonLoaderComponent,
|
||||
ErrorStateComponent,
|
||||
],
|
||||
})
|
||||
export class AttendanceComponent implements OnInit {
|
||||
private route = inject(ActivatedRoute);
|
||||
export class AttendanceComponent {
|
||||
private router = inject(Router);
|
||||
private dialog = inject(MatDialog);
|
||||
private snackBar = inject(MatSnackBar);
|
||||
private auth = inject(AuthService);
|
||||
private ser = inject(AttendanceService);
|
||||
private attendanceTypeSer = inject(AttendanceTypeService);
|
||||
date = input(null, { transform: (v: string | null | undefined) => v ?? null });
|
||||
|
||||
@ViewChild('dateElement', { static: false }) dateElement!: ElementRef<HTMLInputElement>;
|
||||
public attendanceObservable = new BehaviorSubject<AttendanceItem[]>([]);
|
||||
dataSource: AttendanceDataSource = new AttendanceDataSource(this.attendanceObservable);
|
||||
form: FormGroup<{
|
||||
date: FormControl<moment.Moment>;
|
||||
attendances: FormArray<
|
||||
FormGroup<{
|
||||
attendanceType: FormControl<number>;
|
||||
}>
|
||||
>;
|
||||
}>;
|
||||
infoResource = this.ser.get(this.date);
|
||||
info = computed(() => this.infoResource.value() ?? new Attendance());
|
||||
attendanceTypesResource = this.attendanceTypeSer.list();
|
||||
attendanceTypes = computed(() => this.attendanceTypesResource.value() ?? []);
|
||||
|
||||
info: Attendance = new Attendance();
|
||||
attendanceTypes: AttendanceType[] = [];
|
||||
model = linkedSignal({
|
||||
source: this.info,
|
||||
computation: (infoVal): AttendanceFormData => ({
|
||||
date: moment(infoVal.date),
|
||||
attendances: infoVal.body.map((x) => ({
|
||||
attendanceType: x.attendanceType.id,
|
||||
})),
|
||||
}),
|
||||
});
|
||||
|
||||
form = createForm(this.model);
|
||||
|
||||
displayedColumns = ['code', 'name', 'designation', 'department', 'status', 'prints'];
|
||||
|
||||
@HostListener('window:keydown.f2', ['$event'])
|
||||
focusDate(event: Event) {
|
||||
event.preventDefault();
|
||||
this.dateElement.nativeElement.focus();
|
||||
this.dateElement.nativeElement.select();
|
||||
this.form().focusBoundControl();
|
||||
}
|
||||
|
||||
constructor() {
|
||||
this.form = new FormGroup({
|
||||
date: new FormControl(moment(new Date()), { nonNullable: true }),
|
||||
attendances: new FormArray<FormGroup<{ attendanceType: FormControl<number> }>>([]),
|
||||
});
|
||||
}
|
||||
|
||||
ngOnInit() {
|
||||
this.route.data.subscribe((value) => {
|
||||
const data = value as { info: Attendance; attendanceTypes: AttendanceType[] };
|
||||
|
||||
this.info = data.info;
|
||||
this.attendanceTypes = data.attendanceTypes;
|
||||
this.form.controls.date.setValue(moment(this.info.date, 'DD-MMM-YYYY'));
|
||||
this.form.controls.attendances.clear();
|
||||
this.info.body.forEach((x) =>
|
||||
this.form.controls.attendances.push(
|
||||
new FormGroup({
|
||||
attendanceType: new FormControl(x.attendanceType.id, { nonNullable: true }),
|
||||
}),
|
||||
),
|
||||
);
|
||||
this.dataSource = new AttendanceDataSource(this.attendanceObservable);
|
||||
this.attendanceObservable.next(this.info.body);
|
||||
afterNextRender(() => {
|
||||
this.form().focusBoundControl();
|
||||
});
|
||||
}
|
||||
|
||||
getClass(index: number) {
|
||||
const array = this.form.controls.attendances;
|
||||
const id = array.controls[index].value.attendanceType;
|
||||
const { name } = this.attendanceTypes.filter((x) => x.id === id)[0];
|
||||
return name.toLowerCase().replace(/(\s+\+\s+)|(\s+)/g, '-');
|
||||
const array = this.model().attendances;
|
||||
if (!array || !array[index]) return '';
|
||||
const id = array[index].attendanceType;
|
||||
const typeObj = this.attendanceTypes().filter((x) => x.id === id)[0];
|
||||
if (!typeObj) return '';
|
||||
return typeObj.name.toLowerCase().replace(/(\s+\+\s+)|(\s+)/g, '-');
|
||||
}
|
||||
|
||||
show() {
|
||||
const date = moment(this.form.value.date).format('DD-MMM-YYYY');
|
||||
const date = moment(this.model().date).format('DD-MMM-YYYY');
|
||||
this.router.navigate(['/attendance', date]);
|
||||
}
|
||||
|
||||
@@ -116,20 +107,23 @@ export class AttendanceComponent implements OnInit {
|
||||
this.snackBar.open('', 'Success');
|
||||
},
|
||||
error: (error) => {
|
||||
this.snackBar.open(error, 'Danger');
|
||||
this.snackBar.open(error as string, 'Danger');
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
getAttendance(): Attendance {
|
||||
const formModel = this.form.value;
|
||||
this.info.date = moment(formModel.date).format('DD-MMM-YYYY');
|
||||
const array = this.form.controls.attendances;
|
||||
const formModel = this.model();
|
||||
const currentInfo = this.info();
|
||||
currentInfo.date = moment(formModel.date).format('DD-MMM-YYYY');
|
||||
const array = formModel.attendances;
|
||||
if (array) {
|
||||
this.info.body.forEach((item, index) => {
|
||||
item.attendanceType.id = array.controls[index].value.attendanceType ?? 0;
|
||||
currentInfo.body.forEach((item, index) => {
|
||||
if (array[index]) {
|
||||
item.attendanceType.id = array[index].attendanceType ?? 0;
|
||||
}
|
||||
});
|
||||
}
|
||||
return this.info;
|
||||
return currentInfo;
|
||||
}
|
||||
}
|
||||
@@ -1,18 +0,0 @@
|
||||
import { TestBed } from '@angular/core/testing';
|
||||
import { ResolveFn } from '@angular/router';
|
||||
|
||||
import { Attendance } from './attendance';
|
||||
import { attendanceResolver } from './attendance.resolver';
|
||||
|
||||
describe('attendanceResolver', () => {
|
||||
const executeResolver: ResolveFn<Attendance> = (...resolverParameters) =>
|
||||
TestBed.runInInjectionContext(() => attendanceResolver(...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 { Attendance } from './attendance';
|
||||
import { AttendanceService } from './attendance.service';
|
||||
|
||||
export const attendanceResolver: ResolveFn<Attendance> = (route) => {
|
||||
const date = route.paramMap.get('date');
|
||||
return inject(AttendanceService).get(date);
|
||||
};
|
||||
@@ -1,9 +1,7 @@
|
||||
import { Routes } from '@angular/router';
|
||||
|
||||
import { authGuard } from '../auth/auth-guard.service';
|
||||
import { attendanceTypeResolver } from './attendance-type.resolver';
|
||||
import { AttendanceComponent } from './attendance.component';
|
||||
import { attendanceResolver } from './attendance.resolver';
|
||||
|
||||
export const routes: Routes = [
|
||||
{
|
||||
@@ -13,10 +11,6 @@ export const routes: Routes = [
|
||||
data: {
|
||||
permission: 'Attendance',
|
||||
},
|
||||
resolve: {
|
||||
info: attendanceResolver,
|
||||
attendanceTypes: attendanceTypeResolver,
|
||||
},
|
||||
runGuardsAndResolvers: 'always',
|
||||
},
|
||||
{
|
||||
@@ -26,10 +20,6 @@ export const routes: Routes = [
|
||||
data: {
|
||||
permission: 'Attendance',
|
||||
},
|
||||
resolve: {
|
||||
info: attendanceResolver,
|
||||
attendanceTypes: attendanceTypeResolver,
|
||||
},
|
||||
runGuardsAndResolvers: 'always',
|
||||
},
|
||||
];
|
||||
@@ -1,5 +1,5 @@
|
||||
import { HttpClient } from '@angular/common/http';
|
||||
import { inject, Injectable } from '@angular/core';
|
||||
import { HttpClient, httpResource } from '@angular/common/http';
|
||||
import { inject, Injectable, Signal } from '@angular/core';
|
||||
import { Observable } from 'rxjs/internal/Observable';
|
||||
import { catchError } from 'rxjs/operators';
|
||||
|
||||
@@ -7,23 +7,21 @@ import { ErrorLoggerService } from '../core/error-logger.service';
|
||||
import { Attendance } from './attendance';
|
||||
|
||||
const url = '/api/attendance';
|
||||
const serviceName = 'AttendanceService';
|
||||
|
||||
@Injectable({ providedIn: 'root' })
|
||||
export class AttendanceService {
|
||||
private http = inject(HttpClient);
|
||||
private log = inject(ErrorLoggerService);
|
||||
|
||||
get(date: string | null): Observable<Attendance> {
|
||||
const getUrl: string = date === null ? url : `${url}/${date}`;
|
||||
return this.http
|
||||
.get<Attendance>(getUrl)
|
||||
.pipe(catchError(this.log.handleError(serviceName, `get date=${date}`))) as Observable<Attendance>;
|
||||
get(date: Signal<string | null>) {
|
||||
return httpResource<Attendance>(() => {
|
||||
const getUrl: string = date() === null ? url : `${url}/${date()}`;
|
||||
return getUrl;
|
||||
});
|
||||
}
|
||||
|
||||
save(attendance: Attendance): Observable<Attendance> {
|
||||
return this.http
|
||||
.post<Attendance>(`${url}/${attendance.date}`, attendance)
|
||||
.pipe(catchError(this.log.handleError(serviceName, 'save'))) as Observable<Attendance>;
|
||||
.pipe(catchError(this.log.handleError('AttendanceService', 'save'))) as Observable<Attendance>;
|
||||
}
|
||||
}
|
||||
@@ -1,16 +1,16 @@
|
||||
<h2>Login</h2>
|
||||
|
||||
<form [formGroup]="form" class="flex-col">
|
||||
<form class="flex-col" [formRoot]="form">
|
||||
<div class="row-container">
|
||||
<mat-form-field class="flex-auto">
|
||||
<mat-label>Username</mat-label>
|
||||
<input matInput #nameElement formControlName="username" />
|
||||
<input matInput [formField]="form.username" />
|
||||
</mat-form-field>
|
||||
</div>
|
||||
<div class="row-container">
|
||||
<mat-form-field class="flex-auto">
|
||||
<mat-label>Password</mat-label>
|
||||
<input matInput formControlName="password" [type]="hide ? 'password' : 'text'" (keyup.enter)="login()" />
|
||||
<input matInput [formField]="form.password" [type]="hide ? 'password' : 'text'" (keyup.enter)="login()" />
|
||||
<mat-icon matSuffix (click)="hide = !hide">{{ hide ? 'visibility' : 'visibility_off' }}</mat-icon>
|
||||
</mat-form-field>
|
||||
</div>
|
||||
@@ -20,7 +20,7 @@
|
||||
<div class="row-container">
|
||||
<mat-form-field class="flex-auto">
|
||||
<mat-label>Otp</mat-label>
|
||||
<input matInput formControlName="otp" />
|
||||
<input matInput [formField]="form.otp" />
|
||||
</mat-form-field>
|
||||
</div>
|
||||
}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { AfterViewInit, ChangeDetectorRef, Component, ElementRef, inject, OnInit, ViewChild } from '@angular/core';
|
||||
import { FormControl, FormGroup, ReactiveFormsModule, Validators } from '@angular/forms';
|
||||
import { ChangeDetectorRef, Component, inject, OnInit, afterNextRender, linkedSignal } from '@angular/core';
|
||||
import { form as createForm, FormField, FormRoot } from '@angular/forms/signals';
|
||||
import { MatButtonModule } from '@angular/material/button';
|
||||
import { MatDividerModule } from '@angular/material/divider';
|
||||
import { MatFormFieldModule } from '@angular/material/form-field';
|
||||
@@ -11,13 +11,19 @@ import { ActivatedRoute, Router } from '@angular/router';
|
||||
import { CookieService } from '../../shared/cookie.service';
|
||||
import { AuthService } from '../auth.service';
|
||||
|
||||
export interface LoginFormData {
|
||||
username: string;
|
||||
password: string;
|
||||
otp: string;
|
||||
}
|
||||
|
||||
@Component({
|
||||
selector: 'app-login',
|
||||
templateUrl: './login.component.html',
|
||||
styleUrls: ['./login.component.css'],
|
||||
imports: [ReactiveFormsModule, MatFormFieldModule, MatInputModule, MatIconModule, MatDividerModule, MatButtonModule],
|
||||
imports: [FormField, FormRoot, MatFormFieldModule, MatInputModule, MatIconModule, MatDividerModule, MatButtonModule],
|
||||
})
|
||||
export class LoginComponent implements OnInit, AfterViewInit {
|
||||
export class LoginComponent implements OnInit {
|
||||
private route = inject(ActivatedRoute);
|
||||
private auth = inject(AuthService);
|
||||
private router = inject(Router);
|
||||
@@ -25,12 +31,16 @@ export class LoginComponent implements OnInit, AfterViewInit {
|
||||
private cs = inject(CookieService);
|
||||
private cd = inject(ChangeDetectorRef);
|
||||
|
||||
@ViewChild('nameElement', { static: true }) nameElement!: ElementRef<HTMLInputElement>;
|
||||
form: FormGroup<{
|
||||
username: FormControl<string>;
|
||||
password: FormControl<string>;
|
||||
otp: FormControl<number | null>;
|
||||
}>;
|
||||
model = linkedSignal({
|
||||
source: () => null,
|
||||
computation: (): LoginFormData => ({
|
||||
username: '',
|
||||
password: '',
|
||||
otp: '',
|
||||
}),
|
||||
});
|
||||
|
||||
form = createForm(this.model);
|
||||
|
||||
hide: boolean;
|
||||
showOtp: boolean;
|
||||
@@ -40,10 +50,8 @@ export class LoginComponent implements OnInit, AfterViewInit {
|
||||
constructor() {
|
||||
this.hide = true;
|
||||
this.showOtp = false;
|
||||
this.form = new FormGroup({
|
||||
username: new FormControl('', { validators: Validators.required, nonNullable: true }),
|
||||
password: new FormControl('', { validators: Validators.required, nonNullable: true }),
|
||||
otp: new FormControl(0),
|
||||
afterNextRender(() => {
|
||||
this.form().focusBoundControl();
|
||||
});
|
||||
}
|
||||
|
||||
@@ -51,17 +59,11 @@ export class LoginComponent implements OnInit, AfterViewInit {
|
||||
this.returnUrl = this.route.snapshot.queryParams['returnUrl'] || '/';
|
||||
}
|
||||
|
||||
ngAfterViewInit() {
|
||||
setTimeout(() => {
|
||||
this.nameElement.nativeElement.focus();
|
||||
}, 0);
|
||||
}
|
||||
|
||||
login(): void {
|
||||
const formModel = this.form.value;
|
||||
const formModel = this.model();
|
||||
const username = formModel.username ?? '';
|
||||
const password = formModel.password ?? '';
|
||||
const otp = formModel.otp ?? 0;
|
||||
const otp = Number(formModel.otp) || 0;
|
||||
this.auth
|
||||
.login(username, password, otp)
|
||||
// .pipe(first())
|
||||
|
||||
@@ -1,72 +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 } from 'rxjs/operators';
|
||||
|
||||
import { BalanceSheetItem } from './balance-sheet-item';
|
||||
|
||||
/** Simple sort comparator for example ID/Name columns (for client-side sorting). */
|
||||
const compare = (a: string | number, b: string | number, isAsc: boolean) => (a < b ? -1 : 1) * (isAsc ? 1 : -1);
|
||||
export class BalanceSheetDataSource extends DataSource<BalanceSheetItem> {
|
||||
constructor(
|
||||
public data: BalanceSheetItem[],
|
||||
private paginator?: MatPaginator,
|
||||
private sort?: MatSort,
|
||||
) {
|
||||
super();
|
||||
}
|
||||
|
||||
connect(): Observable<BalanceSheetItem[]> {
|
||||
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);
|
||||
}
|
||||
|
||||
// Set the paginators length
|
||||
if (this.paginator) {
|
||||
this.paginator.length = this.data.length;
|
||||
}
|
||||
|
||||
return merge(observableOf(this.data), ...dataMutations).pipe(
|
||||
map(() => this.getPagedData(this.getSortedData([...this.data]))),
|
||||
);
|
||||
}
|
||||
|
||||
disconnect() {}
|
||||
|
||||
private getPagedData(data: BalanceSheetItem[]) {
|
||||
if (this.paginator === undefined) {
|
||||
return data;
|
||||
}
|
||||
const startIndex = this.paginator.pageIndex * this.paginator.pageSize;
|
||||
return data.splice(startIndex, this.paginator.pageSize);
|
||||
}
|
||||
|
||||
private getSortedData(data: BalanceSheetItem[]) {
|
||||
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':
|
||||
if (a.name === undefined || b.name === undefined) {
|
||||
return 0;
|
||||
}
|
||||
return compare(a.name, b.name, isAsc);
|
||||
default:
|
||||
return 0;
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -1,59 +1,65 @@
|
||||
<h2>Balance Sheet</h2>
|
||||
|
||||
<form [formGroup]="form" class="flex-col">
|
||||
<form class="flex-col" [formRoot]="form">
|
||||
<div class="row-container">
|
||||
<mat-form-field class="flex-auto">
|
||||
<mat-label>Date</mat-label>
|
||||
<input matInput #dateElement [matDatepicker]="dateInput" formControlName="date" autocomplete="off" />
|
||||
<input matInput #dateElement [matDatepicker]="dateInput" [formField]="form.date" autocomplete="off" />
|
||||
<mat-datepicker-toggle matSuffix [for]="dateInput"></mat-datepicker-toggle>
|
||||
<mat-datepicker #dateInput></mat-datepicker>
|
||||
</mat-form-field>
|
||||
<button mat-raised-button color="primary" (click)="show()">Show</button>
|
||||
</div>
|
||||
</form>
|
||||
<mat-table #table [dataSource]="dataSource" matSort aria-label="Elements">
|
||||
<!-- Group Column -->
|
||||
<ng-container matColumnDef="group">
|
||||
<mat-header-cell *matHeaderCellDef mat-sort-header>Group</mat-header-cell>
|
||||
<mat-cell *matCellDef="let row">{{ row.group }}</mat-cell>
|
||||
<mat-footer-cell *matFooterCellDef>{{ info.footer?.group }}</mat-footer-cell>
|
||||
</ng-container>
|
||||
@if (infoResource.isLoading()) {
|
||||
<app-skeleton-loader type="table" [count]="1" />
|
||||
} @else if (infoResource.error()) {
|
||||
<app-error-state [message]="'Failed to load data.'" (retryAction)="infoResource.reload()" />
|
||||
} @else {
|
||||
<mat-table #table [dataSource]="dataSource()" matSort (matSortChange)="sortData($event)">
|
||||
<!-- Group Column -->
|
||||
<ng-container matColumnDef="group">
|
||||
<mat-header-cell *matHeaderCellDef mat-sort-header>Group</mat-header-cell>
|
||||
<mat-cell *matCellDef="let row">{{ row.group }}</mat-cell>
|
||||
<mat-footer-cell *matFooterCellDef>{{ info().footer?.group }}</mat-footer-cell>
|
||||
</ng-container>
|
||||
|
||||
<!-- Name Column -->
|
||||
<ng-container matColumnDef="name">
|
||||
<mat-header-cell *matHeaderCellDef mat-sort-header>Name</mat-header-cell>
|
||||
<mat-cell *matCellDef="let row">{{ row.name }}</mat-cell>
|
||||
<mat-footer-cell *matFooterCellDef>{{ info.footer?.name }}</mat-footer-cell>
|
||||
</ng-container>
|
||||
<!-- Name Column -->
|
||||
<ng-container matColumnDef="name">
|
||||
<mat-header-cell *matHeaderCellDef mat-sort-header>Name</mat-header-cell>
|
||||
<mat-cell *matCellDef="let row">{{ row.name }}</mat-cell>
|
||||
<mat-footer-cell *matFooterCellDef>{{ info().footer?.name }}</mat-footer-cell>
|
||||
</ng-container>
|
||||
|
||||
<!-- SubAmount Column -->
|
||||
<ng-container matColumnDef="subAmount">
|
||||
<mat-header-cell *matHeaderCellDef mat-sort-header class="right">Amount</mat-header-cell>
|
||||
<mat-cell *matCellDef="let row" class="right">{{ row.subAmount | currency: 'INR' | clear }}</mat-cell>
|
||||
<mat-footer-cell *matFooterCellDef class="right"
|
||||
>{{ info.footer?.subAmount | currency: 'INR' | clear }}
|
||||
</mat-footer-cell>
|
||||
</ng-container>
|
||||
<!-- SubAmount Column -->
|
||||
<ng-container matColumnDef="subAmount">
|
||||
<mat-header-cell *matHeaderCellDef mat-sort-header class="right">Amount</mat-header-cell>
|
||||
<mat-cell *matCellDef="let row" class="right">{{ row.subAmount | currency: 'INR' | clear }}</mat-cell>
|
||||
<mat-footer-cell *matFooterCellDef class="right"
|
||||
>{{ $safeNavigationMigration(info().footer?.subAmount) | currency: 'INR' | clear }}
|
||||
</mat-footer-cell>
|
||||
</ng-container>
|
||||
|
||||
<!-- Total Column -->
|
||||
<ng-container matColumnDef="total">
|
||||
<mat-header-cell *matHeaderCellDef mat-sort-header class="right">Total</mat-header-cell>
|
||||
<mat-cell *matCellDef="let row" class="right">{{ row.amount | currency: 'INR' | clear }}</mat-cell>
|
||||
<mat-footer-cell *matFooterCellDef class="right"
|
||||
>{{ info.footer?.amount | currency: 'INR' | clear }}
|
||||
</mat-footer-cell>
|
||||
</ng-container>
|
||||
<!-- Total Column -->
|
||||
<ng-container matColumnDef="total">
|
||||
<mat-header-cell *matHeaderCellDef mat-sort-header class="right">Total</mat-header-cell>
|
||||
<mat-cell *matCellDef="let row" class="right">{{ row.amount | currency: 'INR' | clear }}</mat-cell>
|
||||
<mat-footer-cell *matFooterCellDef class="right"
|
||||
>{{ $safeNavigationMigration(info().footer?.amount) | currency: 'INR' | clear }}
|
||||
</mat-footer-cell>
|
||||
</ng-container>
|
||||
|
||||
<mat-header-row *matHeaderRowDef="displayedColumns"></mat-header-row>
|
||||
<mat-row *matRowDef="let row; columns: displayedColumns"></mat-row>
|
||||
<mat-footer-row *matFooterRowDef="displayedColumns"></mat-footer-row>
|
||||
</mat-table>
|
||||
<mat-header-row *matHeaderRowDef="displayedColumns"></mat-header-row>
|
||||
<mat-row *matRowDef="let row; columns: displayedColumns"></mat-row>
|
||||
<mat-footer-row *matFooterRowDef="displayedColumns"></mat-footer-row>
|
||||
</mat-table>
|
||||
|
||||
<mat-paginator
|
||||
#paginator
|
||||
[length]="dataSource.data.length"
|
||||
[pageIndex]="0"
|
||||
[pageSize]="50"
|
||||
[pageSizeOptions]="[25, 50, 100, 250, 300, 5000]"
|
||||
>
|
||||
</mat-paginator>
|
||||
<mat-paginator
|
||||
(page)="handlePageEvent($event)"
|
||||
[length]="(this.info().body ?? []).length"
|
||||
[pageIndex]="pageIndex()"
|
||||
[pageSize]="pageSize()"
|
||||
[pageSizeOptions]="[25, 50, 100, 250, 300, 5000]"
|
||||
>
|
||||
</mat-paginator>
|
||||
}
|
||||
@@ -1,26 +1,32 @@
|
||||
import { CurrencyPipe } from '@angular/common';
|
||||
import { Component, ElementRef, HostListener, inject, OnInit, ViewChild } from '@angular/core';
|
||||
import { FormControl, FormGroup, ReactiveFormsModule } from '@angular/forms';
|
||||
import { Component, HostListener, inject, input, computed, linkedSignal, signal, afterNextRender } from '@angular/core';
|
||||
import { form as createForm, FormField, FormRoot } from '@angular/forms/signals';
|
||||
export interface BalanceSheetFormData {
|
||||
date: moment.Moment;
|
||||
}
|
||||
import { MatButtonModule } from '@angular/material/button';
|
||||
import { MatDatepickerModule } from '@angular/material/datepicker';
|
||||
import { MatFormFieldModule } from '@angular/material/form-field';
|
||||
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, Router } from '@angular/router';
|
||||
import { Router } from '@angular/router';
|
||||
import moment from 'moment';
|
||||
|
||||
import { ClearPipe } from '../shared/clear.pipe';
|
||||
import { ErrorStateComponent } from '../shared/error-state/error-state.component';
|
||||
import { SkeletonLoaderComponent } from '../shared/skeleton-loader/skeleton-loader.component';
|
||||
import { BalanceSheet } from './balance-sheet';
|
||||
import { BalanceSheetDataSource } from './balance-sheet-datasource';
|
||||
import { BalanceSheetService } from './balance-sheet.service';
|
||||
|
||||
@Component({
|
||||
selector: 'app-balance-sheet',
|
||||
templateUrl: './balance-sheet.component.html',
|
||||
styleUrls: ['./balance-sheet.component.css'],
|
||||
imports: [
|
||||
ReactiveFormsModule,
|
||||
FormField,
|
||||
FormRoot,
|
||||
MatFormFieldModule,
|
||||
MatInputModule,
|
||||
MatDatepickerModule,
|
||||
@@ -30,20 +36,69 @@ import { BalanceSheetDataSource } from './balance-sheet-datasource';
|
||||
MatPaginatorModule,
|
||||
CurrencyPipe,
|
||||
ClearPipe,
|
||||
|
||||
SkeletonLoaderComponent,
|
||||
ErrorStateComponent,
|
||||
],
|
||||
})
|
||||
export class BalanceSheetComponent implements OnInit {
|
||||
private route = inject(ActivatedRoute);
|
||||
export class BalanceSheetComponent {
|
||||
private router = inject(Router);
|
||||
private ser = inject(BalanceSheetService);
|
||||
pageSize = signal(50);
|
||||
pageIndex = signal(0);
|
||||
date = input(null, { transform: (v: string | null | undefined) => v ?? null });
|
||||
infoResource = this.ser.list(this.date);
|
||||
info = computed(() => this.infoResource.value() ?? new BalanceSheet());
|
||||
|
||||
@ViewChild(MatPaginator, { static: true }) paginator!: MatPaginator;
|
||||
@ViewChild(MatSort, { static: true }) sort!: MatSort;
|
||||
@ViewChild('dateElement', { static: true }) dateElement!: ElementRef<HTMLInputElement>;
|
||||
info: BalanceSheet = new BalanceSheet();
|
||||
dataSource: BalanceSheetDataSource = new BalanceSheetDataSource(this.info.body, this.paginator, this.sort);
|
||||
form: FormGroup<{
|
||||
date: FormControl<moment.Moment>;
|
||||
}>;
|
||||
sortActive = signal('');
|
||||
sortDirection = signal('');
|
||||
|
||||
formModel = linkedSignal<import('./balance-sheet').BalanceSheet, BalanceSheetFormData>({
|
||||
source: this.info,
|
||||
computation: (info: BalanceSheet): BalanceSheetFormData => ({
|
||||
date: info.date ? moment(info.date, 'DD-MMM-YYYY') : moment(new Date()),
|
||||
}),
|
||||
});
|
||||
|
||||
form = createForm(this.formModel);
|
||||
sortedList = computed(() => {
|
||||
const data = this.info().body ?? [];
|
||||
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 'group':
|
||||
return compare(a.group ?? '', b.group ?? '', isAsc);
|
||||
case 'name':
|
||||
return compare(a.name ?? '', b.name ?? '', isAsc);
|
||||
case 'subAmount':
|
||||
return compare(a.subAmount ?? 0, b.subAmount ?? 0, isAsc);
|
||||
case 'total':
|
||||
return compare(a.amount ?? 0, b.amount ?? 0, 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);
|
||||
});
|
||||
|
||||
constructor() {
|
||||
afterNextRender(() => {
|
||||
this.form().focusBoundControl();
|
||||
});
|
||||
}
|
||||
|
||||
/** Columns displayed in the table. Columns IDs can be added, removed, or reordered. */
|
||||
displayedColumns = ['group', 'name', 'subAmount', 'total'];
|
||||
@@ -51,26 +106,7 @@ export class BalanceSheetComponent implements OnInit {
|
||||
@HostListener('window:keydown.f2', ['$event'])
|
||||
focusDate(event: Event) {
|
||||
event.preventDefault();
|
||||
this.dateElement.nativeElement.focus();
|
||||
this.dateElement.nativeElement.select();
|
||||
}
|
||||
|
||||
constructor() {
|
||||
this.form = new FormGroup({
|
||||
date: new FormControl(moment(new Date()), { nonNullable: true }),
|
||||
});
|
||||
}
|
||||
|
||||
ngOnInit() {
|
||||
this.route.data.subscribe((value) => {
|
||||
const data = value as { info: BalanceSheet };
|
||||
|
||||
this.info = data.info;
|
||||
this.form.setValue({
|
||||
date: moment(this.info.date, 'DD-MMM-YYYY'),
|
||||
});
|
||||
this.dataSource = new BalanceSheetDataSource(this.info.body, this.paginator, this.sort);
|
||||
});
|
||||
this.form().focusBoundControl();
|
||||
}
|
||||
|
||||
show() {
|
||||
@@ -79,7 +115,19 @@ export class BalanceSheetComponent implements OnInit {
|
||||
}
|
||||
|
||||
getDate(): string {
|
||||
const formModel = this.form.value;
|
||||
const formModel = this.formModel();
|
||||
return moment(formModel.date).format('DD-MMM-YYYY');
|
||||
}
|
||||
|
||||
handlePageEvent(e: PageEvent) {
|
||||
this.pageSize.set(e.pageSize);
|
||||
this.pageIndex.set(e.pageIndex);
|
||||
}
|
||||
|
||||
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 { BalanceSheet } from './balance-sheet';
|
||||
import { balanceSheetResolver } from './balance-sheet.resolver';
|
||||
|
||||
describe('balanceSheetResolver', () => {
|
||||
const executeResolver: ResolveFn<BalanceSheet> = (...resolverParameters) =>
|
||||
TestBed.runInInjectionContext(() => balanceSheetResolver(...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 { BalanceSheet } from './balance-sheet';
|
||||
import { BalanceSheetService } from './balance-sheet.service';
|
||||
|
||||
export const balanceSheetResolver: ResolveFn<BalanceSheet> = (route) => {
|
||||
const date = route.paramMap.get('date');
|
||||
return inject(BalanceSheetService).list(date);
|
||||
};
|
||||
@@ -2,7 +2,6 @@ import { Routes } from '@angular/router';
|
||||
|
||||
import { authGuard } from '../auth/auth-guard.service';
|
||||
import { BalanceSheetComponent } from './balance-sheet.component';
|
||||
import { balanceSheetResolver } from './balance-sheet.resolver';
|
||||
|
||||
export const routes: Routes = [
|
||||
{
|
||||
@@ -12,9 +11,6 @@ export const routes: Routes = [
|
||||
data: {
|
||||
permission: 'Balance Sheet',
|
||||
},
|
||||
resolve: {
|
||||
info: balanceSheetResolver,
|
||||
},
|
||||
runGuardsAndResolvers: 'always',
|
||||
},
|
||||
{
|
||||
@@ -24,9 +20,6 @@ export const routes: Routes = [
|
||||
data: {
|
||||
permission: 'Balance Sheet',
|
||||
},
|
||||
resolve: {
|
||||
info: balanceSheetResolver,
|
||||
},
|
||||
runGuardsAndResolvers: 'always',
|
||||
},
|
||||
];
|
||||
@@ -1,25 +1,17 @@
|
||||
import { HttpClient } from '@angular/common/http';
|
||||
import { inject, Injectable } from '@angular/core';
|
||||
import { Observable } from 'rxjs/internal/Observable';
|
||||
import { catchError } from 'rxjs/operators';
|
||||
import { httpResource } from '@angular/common/http';
|
||||
import { Injectable, Signal } from '@angular/core';
|
||||
|
||||
import { ErrorLoggerService } from '../core/error-logger.service';
|
||||
import { BalanceSheet } from './balance-sheet';
|
||||
|
||||
const url = '/api/balance-sheet';
|
||||
const serviceName = 'BalanceSheetService';
|
||||
|
||||
@Injectable({
|
||||
providedIn: 'root',
|
||||
})
|
||||
export class BalanceSheetService {
|
||||
private http = inject(HttpClient);
|
||||
private log = inject(ErrorLoggerService);
|
||||
|
||||
list(date: string | null): Observable<BalanceSheet> {
|
||||
const listUrl = date === null ? url : `${url}/${date}`;
|
||||
return this.http
|
||||
.get<BalanceSheet>(listUrl)
|
||||
.pipe(catchError(this.log.handleError(serviceName, 'list'))) as Observable<BalanceSheet>;
|
||||
list(date: Signal<string | null>) {
|
||||
return httpResource<BalanceSheet>(() => {
|
||||
const listUrl = date() === null ? url : `${url}/${date()}`;
|
||||
return listUrl;
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -1,16 +0,0 @@
|
||||
import { DataSource } from '@angular/cdk/collections';
|
||||
import { Observable, of as observableOf } from 'rxjs';
|
||||
|
||||
import { BatchIntegrity } from './batch-integrity';
|
||||
|
||||
export class BatchIntegrityReportDatasource extends DataSource<BatchIntegrity> {
|
||||
constructor(public data: BatchIntegrity[]) {
|
||||
super();
|
||||
}
|
||||
|
||||
connect(): Observable<BatchIntegrity[]> {
|
||||
return observableOf(this.data);
|
||||
}
|
||||
|
||||
disconnect() {}
|
||||
}
|
||||
@@ -1,30 +1,36 @@
|
||||
<h2>Batch Integrity</h2>
|
||||
|
||||
<mat-table [dataSource]="dataSource" aria-label="Elements">
|
||||
<!-- Batch Column -->
|
||||
<ng-container matColumnDef="batch">
|
||||
<mat-header-cell *matHeaderCellDef>Batch</mat-header-cell>
|
||||
<mat-cell *matCellDef="let row">
|
||||
{{ row.product }} dated {{ row.date }} @ {{ row.price | currency: 'INR' }} is showing
|
||||
{{ row.showing }} instead of {{ row.actual }}
|
||||
</mat-cell>
|
||||
</ng-container>
|
||||
@if (infoResource.isLoading()) {
|
||||
<app-skeleton-loader type="table" [count]="1" />
|
||||
} @else if (infoResource.error()) {
|
||||
<app-error-state [message]="'Failed to load data.'" (retryAction)="infoResource.reload()" />
|
||||
} @else {
|
||||
<mat-table [dataSource]="info()">
|
||||
<!-- Batch Column -->
|
||||
<ng-container matColumnDef="batch">
|
||||
<mat-header-cell *matHeaderCellDef>Batch</mat-header-cell>
|
||||
<mat-cell *matCellDef="let row">
|
||||
{{ row.product }} dated {{ row.date }} @ {{ row.price | currency: 'INR' }} is showing
|
||||
{{ row.showing }} instead of {{ row.actual }}
|
||||
</mat-cell>
|
||||
</ng-container>
|
||||
|
||||
<!-- Details Column -->
|
||||
<ng-container matColumnDef="details">
|
||||
<mat-header-cell *matHeaderCellDef>Details</mat-header-cell>
|
||||
<mat-cell *matCellDef="let row">
|
||||
<ul>
|
||||
@for (detail of row.details; track detail) {
|
||||
<li>
|
||||
<a [routerLink]="detail.url">{{ detail.type }}</a> on {{ detail.date }} of {{ detail.quantity }} @
|
||||
{{ detail.price | currency: 'INR' }}
|
||||
</li>
|
||||
}
|
||||
</ul>
|
||||
</mat-cell>
|
||||
</ng-container>
|
||||
<!-- Details Column -->
|
||||
<ng-container matColumnDef="details">
|
||||
<mat-header-cell *matHeaderCellDef>Details</mat-header-cell>
|
||||
<mat-cell *matCellDef="let row">
|
||||
<ul>
|
||||
@for (detail of row.details; track detail) {
|
||||
<li>
|
||||
<a [routerLink]="detail.url">{{ detail.type }}</a> on {{ detail.date }} of {{ detail.quantity }} @
|
||||
{{ detail.price | currency: 'INR' }}
|
||||
</li>
|
||||
}
|
||||
</ul>
|
||||
</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>
|
||||
}
|
||||
@@ -1,32 +1,27 @@
|
||||
import { CurrencyPipe } from '@angular/common';
|
||||
import { Component, inject, OnInit } from '@angular/core';
|
||||
import { Component, inject, computed } from '@angular/core';
|
||||
import { MatTableModule } from '@angular/material/table';
|
||||
import { ActivatedRoute, Router, RouterModule } from '@angular/router';
|
||||
|
||||
import { BatchIntegrity } from './batch-integrity';
|
||||
import { BatchIntegrityReportDatasource } from './batch-integrity-report-datasource';
|
||||
import { ErrorStateComponent } from '../shared/error-state/error-state.component';
|
||||
import { SkeletonLoaderComponent } from '../shared/skeleton-loader/skeleton-loader.component';
|
||||
import { BatchIntegrityReportService } from './batch-integrity-report.service';
|
||||
|
||||
@Component({
|
||||
selector: 'app-batch-integrity',
|
||||
templateUrl: './batch-integrity-report.component.html',
|
||||
styleUrls: ['./batch-integrity-report.component.css'],
|
||||
imports: [MatTableModule, RouterModule, CurrencyPipe],
|
||||
imports: [MatTableModule, RouterModule, CurrencyPipe, SkeletonLoaderComponent, ErrorStateComponent],
|
||||
})
|
||||
export class BatchIntegrityReportComponent implements OnInit {
|
||||
export class BatchIntegrityReportComponent {
|
||||
private route = inject(ActivatedRoute);
|
||||
private router = inject(Router);
|
||||
private ser = inject(BatchIntegrityReportService);
|
||||
|
||||
info: BatchIntegrity[] = [];
|
||||
dataSource: BatchIntegrityReportDatasource = new BatchIntegrityReportDatasource(this.info);
|
||||
displayedColumns = ['batch', 'details'];
|
||||
|
||||
ngOnInit() {
|
||||
this.route.data.subscribe((value) => {
|
||||
const data = value as { info: BatchIntegrity[] };
|
||||
this.info = data.info;
|
||||
this.dataSource = new BatchIntegrityReportDatasource(this.info);
|
||||
});
|
||||
}
|
||||
infoResource = this.ser.batchIntegrity();
|
||||
info = computed(() => this.infoResource.value() ?? []);
|
||||
|
||||
refresh() {
|
||||
this.router.navigate([]);
|
||||
|
||||
@@ -2,7 +2,6 @@ import { Routes } from '@angular/router';
|
||||
|
||||
import { authGuard } from '../auth/auth-guard.service';
|
||||
import { BatchIntegrityReportComponent } from './batch-integrity-report.component';
|
||||
import { batchIntegrityResolver } from './batch-integrity.resolver';
|
||||
|
||||
export const routes: Routes = [
|
||||
{
|
||||
@@ -12,9 +11,6 @@ export const routes: Routes = [
|
||||
data: {
|
||||
permission: 'Product Ledger',
|
||||
},
|
||||
resolve: {
|
||||
info: batchIntegrityResolver,
|
||||
},
|
||||
runGuardsAndResolvers: 'always',
|
||||
},
|
||||
];
|
||||
@@ -1,24 +1,18 @@
|
||||
import { HttpClient } from '@angular/common/http';
|
||||
import { inject, Injectable } from '@angular/core';
|
||||
import { Observable } from 'rxjs/internal/Observable';
|
||||
import { catchError } from 'rxjs/operators';
|
||||
import { httpResource } from '@angular/common/http';
|
||||
import { Injectable } from '@angular/core';
|
||||
|
||||
import { ErrorLoggerService } from '../core/error-logger.service';
|
||||
import { BatchIntegrity } from './batch-integrity';
|
||||
|
||||
const serviceName = 'BatchIntegrityReportService';
|
||||
|
||||
@Injectable({
|
||||
providedIn: 'root',
|
||||
})
|
||||
export class BatchIntegrityReportService {
|
||||
private http = inject(HttpClient);
|
||||
private log = inject(ErrorLoggerService);
|
||||
|
||||
batchIntegrity(): Observable<BatchIntegrity[]> {
|
||||
batchIntegrity() {
|
||||
const url = '/api/batch-integrity';
|
||||
return this.http
|
||||
.post<BatchIntegrity[]>(url, {})
|
||||
.pipe(catchError(this.log.handleError(serviceName, 'batchIntegrity'))) as Observable<BatchIntegrity[]>;
|
||||
return httpResource<BatchIntegrity[]>(() => ({
|
||||
url,
|
||||
method: 'POST',
|
||||
body: {},
|
||||
}));
|
||||
}
|
||||
}
|
||||
@@ -1,18 +0,0 @@
|
||||
import { TestBed } from '@angular/core/testing';
|
||||
import { ResolveFn } from '@angular/router';
|
||||
|
||||
import { BatchIntegrity } from './batch-integrity';
|
||||
import { batchIntegrityResolver } from './batch-integrity.resolver';
|
||||
|
||||
describe('batchIntegrityResolver', () => {
|
||||
const executeResolver: ResolveFn<BatchIntegrity[]> = (...resolverParameters) =>
|
||||
TestBed.runInInjectionContext(() => batchIntegrityResolver(...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 { BatchIntegrity } from './batch-integrity';
|
||||
import { BatchIntegrityReportService } from './batch-integrity-report.service';
|
||||
|
||||
export const batchIntegrityResolver: ResolveFn<BatchIntegrity[]> = () => {
|
||||
return inject(BatchIntegrityReportService).batchIntegrity();
|
||||
};
|
||||
@@ -1,16 +0,0 @@
|
||||
import { DataSource } from '@angular/cdk/collections';
|
||||
import { Observable, of as observableOf } from 'rxjs';
|
||||
|
||||
import { CashFlowItem } from './cash-flow-item';
|
||||
|
||||
export class CashFlowDataSource extends DataSource<CashFlowItem> {
|
||||
constructor(private data: CashFlowItem[]) {
|
||||
super();
|
||||
}
|
||||
|
||||
connect(): Observable<CashFlowItem[]> {
|
||||
return observableOf(this.data);
|
||||
}
|
||||
|
||||
disconnect() {}
|
||||
}
|
||||
@@ -1,39 +1,45 @@
|
||||
<h2>Cash Flow</h2>
|
||||
|
||||
<form [formGroup]="form" class="flex-col">
|
||||
<form class="flex-col" [formRoot]="form">
|
||||
<div class="row-container">
|
||||
<mat-form-field class="flex-auto basis-2-5">
|
||||
<mat-label>Start Date</mat-label>
|
||||
<input matInput #startDateElement [matDatepicker]="startDate" formControlName="startDate" autocomplete="off" />
|
||||
<mat-datepicker-toggle matSuffix [for]="startDate"></mat-datepicker-toggle>
|
||||
<mat-datepicker #startDate></mat-datepicker>
|
||||
<input matInput [matDatepicker]="startDatePicker" [formField]="form.startDate" autocomplete="off" />
|
||||
<mat-datepicker-toggle matSuffix [for]="startDatePicker"></mat-datepicker-toggle>
|
||||
<mat-datepicker #startDatePicker></mat-datepicker>
|
||||
</mat-form-field>
|
||||
<mat-form-field class="flex-auto basis-2-5">
|
||||
<mat-label>Finish Date</mat-label>
|
||||
<input matInput [matDatepicker]="finishDate" formControlName="finishDate" autocomplete="off" />
|
||||
<input matInput [matDatepicker]="finishDate" [formField]="form.finishDate" autocomplete="off" />
|
||||
<mat-datepicker-toggle matSuffix [for]="finishDate"></mat-datepicker-toggle>
|
||||
<mat-datepicker #finishDate></mat-datepicker>
|
||||
</mat-form-field>
|
||||
<button mat-raised-button color="primary" (click)="show()" class="flex-auto basis-1-5">Show</button>
|
||||
</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]="row.url" [queryParams]="{ startDate: info.startDate, finishDate: info.finishDate }"
|
||||
>{{ row.name }}
|
||||
</a></mat-cell
|
||||
>
|
||||
</ng-container>
|
||||
@if (infoResource.isLoading()) {
|
||||
<app-skeleton-loader type="table" [count]="1" />
|
||||
} @else if (infoResource.error()) {
|
||||
<app-error-state [message]="'Failed to load cash flow.'" (retryAction)="infoResource.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]="row.url" [queryParams]="{ startDate: info().startDate, finishDate: info().finishDate }"
|
||||
>{{ row.name }}
|
||||
</a></mat-cell
|
||||
>
|
||||
</ng-container>
|
||||
|
||||
<!-- Amount Column -->
|
||||
<ng-container matColumnDef="amount">
|
||||
<mat-header-cell *matHeaderCellDef mat-sort-header class="right">Amount</mat-header-cell>
|
||||
<mat-cell *matCellDef="let row" class="right">{{ row.amount | currency: 'INR' | clear }}</mat-cell>
|
||||
</ng-container>
|
||||
<!-- Amount Column -->
|
||||
<ng-container matColumnDef="amount">
|
||||
<mat-header-cell *matHeaderCellDef mat-sort-header class="right">Amount</mat-header-cell>
|
||||
<mat-cell *matCellDef="let row" class="right">{{ row.amount | currency: 'INR' | clear }}</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>
|
||||
}
|
||||
@@ -1,25 +1,32 @@
|
||||
import { CurrencyPipe } from '@angular/common';
|
||||
import { Component, ElementRef, HostListener, inject, OnInit, ViewChild } from '@angular/core';
|
||||
import { FormControl, FormGroup, ReactiveFormsModule } from '@angular/forms';
|
||||
import { Component, HostListener, inject, input, computed, linkedSignal, afterNextRender, signal } from '@angular/core';
|
||||
import { form as createForm, FormField, FormRoot } from '@angular/forms/signals';
|
||||
export interface CashFlowFormData {
|
||||
startDate: moment.Moment;
|
||||
finishDate: moment.Moment;
|
||||
}
|
||||
import { MatButtonModule } from '@angular/material/button';
|
||||
import { MatDatepickerModule } from '@angular/material/datepicker';
|
||||
import { MatFormFieldModule } from '@angular/material/form-field';
|
||||
import { MatInputModule } from '@angular/material/input';
|
||||
import { MatSortModule } from '@angular/material/sort';
|
||||
import { MatSortModule, Sort } from '@angular/material/sort';
|
||||
import { MatTableModule } from '@angular/material/table';
|
||||
import { ActivatedRoute, Router, RouterModule } from '@angular/router';
|
||||
import { Router, RouterModule } from '@angular/router';
|
||||
import moment from 'moment';
|
||||
|
||||
import { ClearPipe } from '../shared/clear.pipe';
|
||||
import { ErrorStateComponent } from '../shared/error-state/error-state.component';
|
||||
import { SkeletonLoaderComponent } from '../shared/skeleton-loader/skeleton-loader.component';
|
||||
import { CashFlow } from './cash-flow';
|
||||
import { CashFlowDataSource } from './cash-flow-datasource';
|
||||
import { CashFlowService } from './cash-flow.service';
|
||||
|
||||
@Component({
|
||||
selector: 'app-cash-flow',
|
||||
templateUrl: './cash-flow.component.html',
|
||||
styleUrls: ['./cash-flow.component.css'],
|
||||
imports: [
|
||||
ReactiveFormsModule,
|
||||
FormField,
|
||||
FormRoot,
|
||||
MatFormFieldModule,
|
||||
MatInputModule,
|
||||
MatDatepickerModule,
|
||||
@@ -29,19 +36,54 @@ import { CashFlowDataSource } from './cash-flow-datasource';
|
||||
RouterModule,
|
||||
CurrencyPipe,
|
||||
ClearPipe,
|
||||
SkeletonLoaderComponent,
|
||||
ErrorStateComponent,
|
||||
],
|
||||
})
|
||||
export class CashFlowComponent implements OnInit {
|
||||
private route = inject(ActivatedRoute);
|
||||
export class CashFlowComponent {
|
||||
private router = inject(Router);
|
||||
private ser = inject(CashFlowService);
|
||||
id = input(null, { transform: (v: string | null | undefined) => v ?? null });
|
||||
startDate = input(null, { transform: (v: string | null | undefined) => v ?? null });
|
||||
finishDate = input(null, { transform: (v: string | null | undefined) => v ?? null });
|
||||
sortActive = signal('');
|
||||
sortDirection = signal('');
|
||||
|
||||
@ViewChild('startDateElement', { static: true }) startDate!: ElementRef<HTMLInputElement>;
|
||||
info: CashFlow = new CashFlow();
|
||||
dataSource: CashFlowDataSource = new CashFlowDataSource(CashFlow.Data(this.info));
|
||||
form: FormGroup<{
|
||||
startDate: FormControl<moment.Moment>;
|
||||
finishDate: FormControl<moment.Moment>;
|
||||
}>;
|
||||
infoResource = this.ser.list(this.id, this.startDate, this.finishDate);
|
||||
info = computed(() => this.infoResource.value() ?? new CashFlow());
|
||||
sortedList = computed(() => {
|
||||
const data = CashFlow.Data(this.info());
|
||||
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 'amount':
|
||||
return compare(a.amount ?? 0, b.amount ?? 0, isAsc);
|
||||
default:
|
||||
return 0;
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
dataSource = computed(() => this.sortedList());
|
||||
|
||||
model = linkedSignal({
|
||||
source: this.info,
|
||||
computation: (info: CashFlow): CashFlowFormData => ({
|
||||
startDate: info.startDate ? moment(info.startDate, 'DD-MMM-YYYY') : moment(new Date()),
|
||||
finishDate: info.finishDate ? moment(info.finishDate, 'DD-MMM-YYYY') : moment(new Date()),
|
||||
}),
|
||||
});
|
||||
|
||||
form = createForm(this.model);
|
||||
|
||||
/** Columns displayed in the table. Columns IDs can be added, removed, or reordered. */
|
||||
displayedColumns = ['name', 'amount'];
|
||||
@@ -49,27 +91,13 @@ export class CashFlowComponent implements OnInit {
|
||||
@HostListener('window:keydown.f2', ['$event'])
|
||||
focusDate(event: Event) {
|
||||
event.preventDefault();
|
||||
this.startDate.nativeElement.focus();
|
||||
this.startDate.nativeElement.select();
|
||||
// TODO: also select the contents
|
||||
this.form.startDate().focusBoundControl();
|
||||
}
|
||||
|
||||
constructor() {
|
||||
this.form = new FormGroup({
|
||||
startDate: new FormControl(moment(new Date()), { nonNullable: true }),
|
||||
finishDate: new FormControl(moment(new Date()), { nonNullable: true }),
|
||||
});
|
||||
}
|
||||
|
||||
ngOnInit() {
|
||||
this.route.data.subscribe((value) => {
|
||||
const data = value as { info: CashFlow };
|
||||
|
||||
this.info = data.info;
|
||||
this.form.setValue({
|
||||
startDate: moment(this.info.startDate, 'DD-MMM-YYYY'),
|
||||
finishDate: moment(this.info.finishDate, 'DD-MMM-YYYY'),
|
||||
});
|
||||
this.dataSource = new CashFlowDataSource(CashFlow.Data(this.info));
|
||||
afterNextRender(() => {
|
||||
this.form.startDate().focusBoundControl();
|
||||
});
|
||||
}
|
||||
|
||||
@@ -84,11 +112,18 @@ export class CashFlowComponent implements OnInit {
|
||||
}
|
||||
|
||||
getInfo(): CashFlow {
|
||||
const formModel = this.form.value;
|
||||
const formModel = this.model();
|
||||
|
||||
return new CashFlow({
|
||||
startDate: moment(formModel.startDate).format('DD-MMM-YYYY'),
|
||||
finishDate: moment(formModel.finishDate).format('DD-MMM-YYYY'),
|
||||
});
|
||||
}
|
||||
|
||||
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 { CashFlow } from './cash-flow';
|
||||
import { cashFlowResolver } from './cash-flow.resolver';
|
||||
|
||||
describe('cashFlowResolver', () => {
|
||||
const executeResolver: ResolveFn<CashFlow> = (...resolverParameters) =>
|
||||
TestBed.runInInjectionContext(() => cashFlowResolver(...resolverParameters));
|
||||
|
||||
beforeEach(() => {
|
||||
TestBed.configureTestingModule({});
|
||||
});
|
||||
|
||||
it('should be created', () => {
|
||||
expect(executeResolver).toBeTruthy();
|
||||
});
|
||||
});
|
||||
@@ -1,12 +0,0 @@
|
||||
import { inject } from '@angular/core';
|
||||
import { ResolveFn } from '@angular/router';
|
||||
|
||||
import { CashFlow } from './cash-flow';
|
||||
import { CashFlowService } from './cash-flow.service';
|
||||
|
||||
export const cashFlowResolver: ResolveFn<CashFlow> = (route) => {
|
||||
const id = route.paramMap.get('id');
|
||||
const startDate = route.queryParamMap.get('startDate') || null;
|
||||
const finishDate = route.queryParamMap.get('finishDate') || null;
|
||||
return inject(CashFlowService).list(id, startDate, finishDate);
|
||||
};
|
||||
@@ -2,7 +2,6 @@ import { Routes } from '@angular/router';
|
||||
|
||||
import { authGuard } from '../auth/auth-guard.service';
|
||||
import { CashFlowComponent } from './cash-flow.component';
|
||||
import { cashFlowResolver } from './cash-flow.resolver';
|
||||
|
||||
export const routes: Routes = [
|
||||
{
|
||||
@@ -12,9 +11,6 @@ export const routes: Routes = [
|
||||
data: {
|
||||
permission: 'Cash Flow',
|
||||
},
|
||||
resolve: {
|
||||
info: cashFlowResolver,
|
||||
},
|
||||
runGuardsAndResolvers: 'always',
|
||||
},
|
||||
{
|
||||
@@ -24,9 +20,6 @@ export const routes: Routes = [
|
||||
data: {
|
||||
permission: 'Cash Flow',
|
||||
},
|
||||
resolve: {
|
||||
info: cashFlowResolver,
|
||||
},
|
||||
runGuardsAndResolvers: 'always',
|
||||
},
|
||||
];
|
||||
@@ -1,39 +1,29 @@
|
||||
import { HttpClient, HttpParams } from '@angular/common/http';
|
||||
import { inject, Injectable } from '@angular/core';
|
||||
import { Observable } from 'rxjs/internal/Observable';
|
||||
import { catchError } from 'rxjs/operators';
|
||||
import { httpResource } from '@angular/common/http';
|
||||
import { Injectable, Signal } from '@angular/core';
|
||||
|
||||
import { ErrorLoggerService } from '../core/error-logger.service';
|
||||
import { CashFlow } from './cash-flow';
|
||||
|
||||
const url = '/api/cash-flow';
|
||||
const serviceName = 'CashFlowService';
|
||||
|
||||
@Injectable({
|
||||
providedIn: 'root',
|
||||
})
|
||||
export class CashFlowService {
|
||||
private http = inject(HttpClient);
|
||||
private log = inject(ErrorLoggerService);
|
||||
|
||||
list(id: string | null, startDate: string | null, finishDate: string | null): Observable<CashFlow> {
|
||||
const options = { params: new HttpParams() };
|
||||
if (startDate !== null) {
|
||||
options.params = options.params.set('s', startDate);
|
||||
}
|
||||
if (finishDate !== null) {
|
||||
options.params = options.params.set('f', finishDate);
|
||||
}
|
||||
let listUrl = '';
|
||||
if (id !== null) {
|
||||
listUrl = `${url}/${id}`;
|
||||
} else if (startDate || finishDate) {
|
||||
listUrl = `${url}/data`;
|
||||
} else {
|
||||
listUrl = url;
|
||||
}
|
||||
return this.http
|
||||
.get<CashFlow>(listUrl, options)
|
||||
.pipe(catchError(this.log.handleError(serviceName, 'list'))) as Observable<CashFlow>;
|
||||
list(id: Signal<string | null>, startDate: Signal<string | null>, finishDate: Signal<string | null>) {
|
||||
return httpResource<CashFlow>(() => {
|
||||
const params: Record<string, string> = {};
|
||||
const startDate_val = startDate();
|
||||
if (startDate_val !== null) params['s'] = startDate_val;
|
||||
const finishDate_val = finishDate();
|
||||
if (finishDate_val !== null) params['f'] = finishDate_val;
|
||||
let resultUrl: string;
|
||||
if (id() !== null) {
|
||||
resultUrl = `${url}/${id()}`;
|
||||
} else if (startDate() || finishDate()) {
|
||||
resultUrl = `${url}/data`;
|
||||
} else {
|
||||
resultUrl = url;
|
||||
}
|
||||
return { url: resultUrl, params };
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -1,30 +1,37 @@
|
||||
<h2>Client</h2>
|
||||
|
||||
<form [formGroup]="form" class="flex-col wrapped">
|
||||
<div class="row-container">
|
||||
<mat-form-field class="flex-auto">
|
||||
<mat-label>Code</mat-label>
|
||||
<input matInput formControlName="code" />
|
||||
</mat-form-field>
|
||||
</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-checkbox formControlName="enabled" class="flex-auto">Is Enabled?</mat-checkbox>
|
||||
</div>
|
||||
<div class="row-container">
|
||||
<mat-form-field class="flex-auto">
|
||||
<mat-label>OTP</mat-label>
|
||||
<input matInput formControlName="otp" />
|
||||
</mat-form-field>
|
||||
</div>
|
||||
</form>
|
||||
@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]="form">
|
||||
<div class="row-container">
|
||||
<mat-form-field class="flex-auto">
|
||||
<mat-label>Code</mat-label>
|
||||
<input matInput [formField]="form.code" />
|
||||
</mat-form-field>
|
||||
</div>
|
||||
<div class="row-container">
|
||||
<mat-form-field class="flex-auto">
|
||||
<mat-label>Name</mat-label>
|
||||
<input matInput [formField]="form.name" />
|
||||
</mat-form-field>
|
||||
</div>
|
||||
<div class="row-container">
|
||||
<mat-checkbox [formField]="form.enabled" class="flex-auto">Is Enabled?</mat-checkbox>
|
||||
</div>
|
||||
<div class="row-container">
|
||||
<mat-form-field class="flex-auto">
|
||||
<mat-label>OTP</mat-label>
|
||||
<input matInput [formField]="form.otp" />
|
||||
</mat-form-field>
|
||||
</div>
|
||||
</form>
|
||||
|
||||
<div class="row-container">
|
||||
<button mat-raised-button color="primary" (click)="save()">Save</button>
|
||||
<button mat-raised-button color="warn" (click)="confirmDelete()">Delete</button>
|
||||
</div>
|
||||
<div class="row-container">
|
||||
<button mat-raised-button color="primary" (click)="save()">Save</button>
|
||||
<button mat-raised-button color="warn" (click)="confirmDelete()">Delete</button>
|
||||
</div>
|
||||
}
|
||||
@@ -1,73 +1,71 @@
|
||||
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 as createForm, FormField, FormRoot } from '@angular/forms/signals';
|
||||
import { MatButtonModule } from '@angular/material/button';
|
||||
import { MatCheckboxModule } from '@angular/material/checkbox';
|
||||
import { MatDialog } from '@angular/material/dialog';
|
||||
import { MatFormFieldModule } from '@angular/material/form-field';
|
||||
import { MatInputModule } from '@angular/material/input';
|
||||
import { MatSnackBar } from '@angular/material/snack-bar';
|
||||
import { ActivatedRoute, Router } from '@angular/router';
|
||||
import { Router } from '@angular/router';
|
||||
|
||||
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 { Client } from '../client';
|
||||
import { ClientService } from '../client.service';
|
||||
|
||||
export interface ClientFormData {
|
||||
code: string;
|
||||
name: string;
|
||||
enabled: boolean;
|
||||
otp: string;
|
||||
}
|
||||
|
||||
@Component({
|
||||
selector: 'app-client-detail',
|
||||
templateUrl: './client-detail.component.html',
|
||||
styleUrls: ['./client-detail.component.css'],
|
||||
imports: [ReactiveFormsModule, MatFormFieldModule, MatInputModule, MatCheckboxModule, MatButtonModule],
|
||||
imports: [
|
||||
FormField,
|
||||
FormRoot,
|
||||
MatFormFieldModule,
|
||||
MatInputModule,
|
||||
MatCheckboxModule,
|
||||
MatButtonModule,
|
||||
SkeletonLoaderComponent,
|
||||
ErrorStateComponent,
|
||||
],
|
||||
})
|
||||
export class ClientDetailComponent implements OnInit, AfterViewInit {
|
||||
private route = inject(ActivatedRoute);
|
||||
export class ClientDetailComponent {
|
||||
id = input(null, { transform: (v: string | null | undefined) => v ?? null });
|
||||
|
||||
private router = inject(Router);
|
||||
private dialog = inject(MatDialog);
|
||||
private snackBar = inject(MatSnackBar);
|
||||
private ser = inject(ClientService);
|
||||
|
||||
@ViewChild('nameElement', { static: true }) nameElement!: ElementRef<HTMLInputElement>;
|
||||
form: FormGroup<{
|
||||
code: FormControl<number | string>;
|
||||
name: FormControl<string | null>;
|
||||
enabled: FormControl<boolean>;
|
||||
otp: FormControl<number | null>;
|
||||
}>;
|
||||
itemResource = this.ser.get(this.id);
|
||||
|
||||
item: Client = new Client();
|
||||
item = computed(() => this.itemResource.value() ?? new Client());
|
||||
|
||||
model = linkedSignal({
|
||||
source: this.item,
|
||||
computation: (item): ClientFormData => ({
|
||||
code: item.code ? String(item.code) : '(Auto)',
|
||||
name: item.name ?? '',
|
||||
enabled: item.enabled ?? false,
|
||||
otp: item.otp ? String(item.otp) : '',
|
||||
}),
|
||||
});
|
||||
|
||||
form = createForm(this.model);
|
||||
|
||||
constructor() {
|
||||
this.form = new FormGroup({
|
||||
code: new FormControl<number | string>({ value: '(Auto)', disabled: true }, { nonNullable: true }),
|
||||
name: new FormControl<string | null>(null),
|
||||
enabled: new FormControl<boolean>(false, { nonNullable: true }),
|
||||
otp: new FormControl<number | null>(null),
|
||||
afterNextRender(() => {
|
||||
this.form.name().focusBoundControl();
|
||||
});
|
||||
}
|
||||
|
||||
ngOnInit() {
|
||||
this.route.data.subscribe((value) => {
|
||||
const data = value as { item: Client };
|
||||
|
||||
this.showItem(data.item);
|
||||
});
|
||||
}
|
||||
|
||||
showItem(item: Client) {
|
||||
this.item = item;
|
||||
this.form.setValue({
|
||||
code: this.item.code || '(Auto)',
|
||||
name: this.item.name,
|
||||
enabled: this.item.enabled,
|
||||
otp: this.item.otp || null,
|
||||
});
|
||||
}
|
||||
|
||||
ngAfterViewInit() {
|
||||
setTimeout(() => {
|
||||
this.nameElement.nativeElement.focus();
|
||||
}, 0);
|
||||
}
|
||||
|
||||
save() {
|
||||
this.ser.update(this.getItem()).subscribe({
|
||||
next: () => {
|
||||
@@ -75,19 +73,19 @@ export class ClientDetailComponent implements OnInit, AfterViewInit {
|
||||
this.router.navigateByUrl('/clients');
|
||||
},
|
||||
error: (error) => {
|
||||
this.snackBar.open(error, 'Danger');
|
||||
this.snackBar.open(error as string, 'Danger');
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
delete() {
|
||||
this.ser.delete(this.item.id).subscribe({
|
||||
this.ser.delete(this.item().id).subscribe({
|
||||
next: () => {
|
||||
this.snackBar.open('', 'Success');
|
||||
this.router.navigateByUrl('/clients');
|
||||
},
|
||||
error: (error) => {
|
||||
this.snackBar.open(error, 'Danger');
|
||||
this.snackBar.open(error as string, 'Danger');
|
||||
},
|
||||
});
|
||||
}
|
||||
@@ -106,10 +104,11 @@ export class ClientDetailComponent implements OnInit, AfterViewInit {
|
||||
}
|
||||
|
||||
getItem(): Client {
|
||||
const formModel = this.form.value;
|
||||
this.item.name = formModel.name ?? '';
|
||||
this.item.enabled = formModel.enabled ?? false;
|
||||
this.item.otp = formModel.otp || undefined;
|
||||
return this.item;
|
||||
const formModel = this.model();
|
||||
const item = this.item();
|
||||
item.name = formModel.name ?? '';
|
||||
item.enabled = formModel.enabled ?? false;
|
||||
item.otp = formModel.otp ? Number(formModel.otp) : undefined;
|
||||
return item;
|
||||
}
|
||||
}
|
||||
@@ -1,18 +0,0 @@
|
||||
import { TestBed } from '@angular/core/testing';
|
||||
import { ResolveFn } from '@angular/router';
|
||||
|
||||
import { Client } from './client';
|
||||
import { clientListResolver } from './client-list.resolver';
|
||||
|
||||
describe('clientListResolver', () => {
|
||||
const executeResolver: ResolveFn<Client[]> = (...resolverParameters) =>
|
||||
TestBed.runInInjectionContext(() => clientListResolver(...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 { Client } from './client';
|
||||
import { ClientService } from './client.service';
|
||||
|
||||
export const clientListResolver: ResolveFn<Client[]> = () => {
|
||||
return inject(ClientService).list();
|
||||
};
|
||||
@@ -1,71 +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 } from 'rxjs/operators';
|
||||
|
||||
import { Client } from '../client';
|
||||
|
||||
/** Simple sort comparator for example ID/Name columns (for client-side sorting). */
|
||||
const compare = (a: string | number, b: string | number, isAsc: boolean) => (a < b ? -1 : 1) * (isAsc ? 1 : -1);
|
||||
export class ClientListDataSource extends DataSource<Client> {
|
||||
constructor(
|
||||
public data: Client[],
|
||||
private paginator?: MatPaginator,
|
||||
private sort?: MatSort,
|
||||
) {
|
||||
super();
|
||||
}
|
||||
|
||||
connect(): Observable<Client[]> {
|
||||
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);
|
||||
}
|
||||
|
||||
// Set the paginators length
|
||||
if (this.paginator) {
|
||||
this.paginator.length = this.data.length;
|
||||
}
|
||||
|
||||
return merge(observableOf(this.data), ...dataMutations).pipe(
|
||||
map(() => this.getPagedData(this.getSortedData([...this.data]))),
|
||||
);
|
||||
}
|
||||
|
||||
disconnect() {}
|
||||
|
||||
private getPagedData(data: Client[]) {
|
||||
if (this.paginator === undefined) {
|
||||
return data;
|
||||
}
|
||||
const startIndex = this.paginator.pageIndex * this.paginator.pageSize;
|
||||
return data.splice(startIndex, this.paginator.pageSize);
|
||||
}
|
||||
|
||||
private getSortedData(data: Client[]) {
|
||||
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 'id':
|
||||
return compare(+a.id, +b.id, isAsc);
|
||||
default:
|
||||
return 0;
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -1,55 +1,61 @@
|
||||
<h2>Clients</h2>
|
||||
|
||||
<mat-table #table [dataSource]="dataSource" matSort aria-label="Elements">
|
||||
<!-- Code Column -->
|
||||
<ng-container matColumnDef="code">
|
||||
<mat-header-cell *matHeaderCellDef mat-sort-header>Code</mat-header-cell>
|
||||
<mat-cell *matCellDef="let row">{{ row.code }}</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)">
|
||||
<!-- Code Column -->
|
||||
<ng-container matColumnDef="code">
|
||||
<mat-header-cell *matHeaderCellDef mat-sort-header>Code</mat-header-cell>
|
||||
<mat-cell *matCellDef="let row">{{ row.code }}</mat-cell>
|
||||
</ng-container>
|
||||
|
||||
<!-- Name Column -->
|
||||
<ng-container matColumnDef="name">
|
||||
<mat-header-cell *matHeaderCellDef mat-sort-header>Name</mat-header-cell>
|
||||
<mat-cell *matCellDef="let row"
|
||||
><a [routerLink]="['/clients', row.id]">{{ row.name }}</a></mat-cell
|
||||
>
|
||||
</ng-container>
|
||||
<!-- Name Column -->
|
||||
<ng-container matColumnDef="name">
|
||||
<mat-header-cell *matHeaderCellDef mat-sort-header>Name</mat-header-cell>
|
||||
<mat-cell *matCellDef="let row"
|
||||
><a [routerLink]="['/clients', row.id]">{{ row.name }}</a></mat-cell
|
||||
>
|
||||
</ng-container>
|
||||
|
||||
<!-- Enabled Column -->
|
||||
<ng-container matColumnDef="enabled">
|
||||
<mat-header-cell *matHeaderCellDef mat-sort-header>Is Enabled?</mat-header-cell>
|
||||
<mat-cell *matCellDef="let row">{{ row.enabled }}</mat-cell>
|
||||
</ng-container>
|
||||
<!-- Enabled Column -->
|
||||
<ng-container matColumnDef="enabled">
|
||||
<mat-header-cell *matHeaderCellDef mat-sort-header>Is Enabled?</mat-header-cell>
|
||||
<mat-cell *matCellDef="let row">{{ row.enabled }}</mat-cell>
|
||||
</ng-container>
|
||||
|
||||
<!-- OTP Column -->
|
||||
<ng-container matColumnDef="otp">
|
||||
<mat-header-cell *matHeaderCellDef mat-sort-header>otp</mat-header-cell>
|
||||
<mat-cell *matCellDef="let row">{{ row.otp }}</mat-cell>
|
||||
</ng-container>
|
||||
<!-- OTP Column -->
|
||||
<ng-container matColumnDef="otp">
|
||||
<mat-header-cell *matHeaderCellDef mat-sort-header>otp</mat-header-cell>
|
||||
<mat-cell *matCellDef="let row">{{ row.otp }}</mat-cell>
|
||||
</ng-container>
|
||||
|
||||
<!-- Created Column -->
|
||||
<ng-container matColumnDef="created">
|
||||
<mat-header-cell *matHeaderCellDef mat-sort-header>Created</mat-header-cell>
|
||||
<mat-cell *matCellDef="let row">{{ row.creationDate | localTime }}</mat-cell>
|
||||
</ng-container>
|
||||
<!-- Created Column -->
|
||||
<ng-container matColumnDef="created">
|
||||
<mat-header-cell *matHeaderCellDef mat-sort-header>Created</mat-header-cell>
|
||||
<mat-cell *matCellDef="let row">{{ row.creationDate | localTime }}</mat-cell>
|
||||
</ng-container>
|
||||
|
||||
<!-- Last Login Column -->
|
||||
<ng-container matColumnDef="last">
|
||||
<mat-header-cell *matHeaderCellDef mat-sort-header>Last Login</mat-header-cell>
|
||||
<mat-cell *matCellDef="let row"
|
||||
>{{ row.lastUser }} @ {{ row.lastDate ? (row.lastDate | localTime) : 'Never' }}</mat-cell
|
||||
>
|
||||
</ng-container>
|
||||
<!-- Last Login Column -->
|
||||
<ng-container matColumnDef="last">
|
||||
<mat-header-cell *matHeaderCellDef mat-sort-header>Last Login</mat-header-cell>
|
||||
<mat-cell *matCellDef="let row"
|
||||
>{{ row.lastUser }} @ {{ row.lastDate ? (row.lastDate | localTime) : 'Never' }}</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]="list().length"
|
||||
[pageIndex]="pageIndex()"
|
||||
[pageSize]="pageSize()"
|
||||
[pageSizeOptions]="[25, 50, 100, 250]"
|
||||
>
|
||||
</mat-paginator>
|
||||
}
|
||||
@@ -1,35 +1,81 @@
|
||||
import { Component, inject, OnInit, ViewChild } from '@angular/core';
|
||||
import { MatPaginator, MatPaginatorModule } from '@angular/material/paginator';
|
||||
import { MatSort, MatSortModule } from '@angular/material/sort';
|
||||
import { Component, computed, inject, signal } from '@angular/core';
|
||||
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 { RouterModule } from '@angular/router';
|
||||
|
||||
import { ErrorStateComponent } from '../../shared/error-state/error-state.component';
|
||||
import { LocalTimePipe } from '../../shared/local-time.pipe';
|
||||
import { Client } from '../client';
|
||||
import { ClientListDataSource } from './client-list-datasource';
|
||||
import { SkeletonLoaderComponent } from '../../shared/skeleton-loader/skeleton-loader.component';
|
||||
import { ClientService } from '../client.service';
|
||||
|
||||
@Component({
|
||||
selector: 'app-client-list',
|
||||
templateUrl: './client-list.component.html',
|
||||
styleUrls: ['./client-list.component.css'],
|
||||
imports: [MatTableModule, MatSortModule, RouterModule, MatPaginatorModule, LocalTimePipe],
|
||||
imports: [
|
||||
MatTableModule,
|
||||
MatSortModule,
|
||||
RouterModule,
|
||||
MatPaginatorModule,
|
||||
LocalTimePipe,
|
||||
SkeletonLoaderComponent,
|
||||
ErrorStateComponent,
|
||||
],
|
||||
})
|
||||
export class ClientListComponent implements OnInit {
|
||||
private route = inject(ActivatedRoute);
|
||||
export class ClientListComponent {
|
||||
private ser = inject(ClientService);
|
||||
|
||||
pageSize = signal(50);
|
||||
pageIndex = signal(0);
|
||||
sortActive = signal('');
|
||||
sortDirection = signal('');
|
||||
listResource = this.ser.list();
|
||||
list = computed(() => this.listResource.value() ?? []);
|
||||
sortedList = computed(() => {
|
||||
const data = this.list();
|
||||
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 'enabled':
|
||||
return compare(a.enabled, b.enabled, isAsc);
|
||||
case 'creationDate':
|
||||
return compare(a.creationDate, b.creationDate, isAsc);
|
||||
case 'lastDate':
|
||||
return compare(a.lastDate ?? 0, b.lastDate ?? 0, 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);
|
||||
});
|
||||
|
||||
@ViewChild(MatPaginator, { static: true }) paginator!: MatPaginator;
|
||||
@ViewChild(MatSort, { static: true }) sort!: MatSort;
|
||||
list: Client[] = [];
|
||||
dataSource: ClientListDataSource = new ClientListDataSource(this.list, this.paginator, this.sort);
|
||||
/** Columns displayed in the table. Columns IDs can be added, removed, or reordered. */
|
||||
displayedColumns = ['code', 'name', 'enabled', 'otp', 'created', 'last'];
|
||||
handlePageEvent(e: PageEvent) {
|
||||
this.pageSize.set(e.pageSize);
|
||||
this.pageIndex.set(e.pageIndex);
|
||||
}
|
||||
|
||||
ngOnInit() {
|
||||
this.route.data.subscribe((value) => {
|
||||
const data = value as { list: Client[] };
|
||||
|
||||
this.list = data.list;
|
||||
});
|
||||
this.dataSource = new ClientListDataSource(this.list, this.paginator, this.sort);
|
||||
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 { Client } from './client';
|
||||
import { clientResolver } from './client.resolver';
|
||||
|
||||
describe('clientResolver', () => {
|
||||
const executeResolver: ResolveFn<Client> = (...resolverParameters) =>
|
||||
TestBed.runInInjectionContext(() => clientResolver(...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 { Client } from './client';
|
||||
import { ClientService } from './client.service';
|
||||
|
||||
export const clientResolver: ResolveFn<Client> = (route) => {
|
||||
const id = route.paramMap.get('id');
|
||||
return inject(ClientService).get(id);
|
||||
};
|
||||
@@ -2,9 +2,7 @@ import { Routes } from '@angular/router';
|
||||
|
||||
import { authGuard } from '../auth/auth-guard.service';
|
||||
import { ClientDetailComponent } from './client-detail/client-detail.component';
|
||||
import { clientListResolver } from './client-list.resolver';
|
||||
import { ClientListComponent } from './client-list/client-list.component';
|
||||
import { clientResolver } from './client.resolver';
|
||||
|
||||
export const routes: Routes = [
|
||||
{
|
||||
@@ -14,9 +12,6 @@ export const routes: Routes = [
|
||||
data: {
|
||||
permission: 'Cost Centres',
|
||||
},
|
||||
resolve: {
|
||||
list: clientListResolver,
|
||||
},
|
||||
},
|
||||
{
|
||||
path: ':id',
|
||||
@@ -25,8 +20,5 @@ export const routes: Routes = [
|
||||
data: {
|
||||
permission: 'Cost Centres',
|
||||
},
|
||||
resolve: {
|
||||
item: clientResolver,
|
||||
},
|
||||
},
|
||||
];
|
||||
@@ -1,5 +1,5 @@
|
||||
import { HttpClient } from '@angular/common/http';
|
||||
import { inject, Injectable } from '@angular/core';
|
||||
import { HttpClient, httpResource } from '@angular/common/http';
|
||||
import { inject, Injectable, Signal } from '@angular/core';
|
||||
import { Observable } from 'rxjs/internal/Observable';
|
||||
import { catchError } from 'rxjs/operators';
|
||||
|
||||
@@ -7,8 +7,6 @@ import { ErrorLoggerService } from '../core/error-logger.service';
|
||||
import { Client } from './client';
|
||||
|
||||
const url = '/api/clients';
|
||||
const serviceName = 'ClientService';
|
||||
|
||||
@Injectable({
|
||||
providedIn: 'root',
|
||||
})
|
||||
@@ -16,28 +14,26 @@ export class ClientService {
|
||||
private http = inject(HttpClient);
|
||||
private log = inject(ErrorLoggerService);
|
||||
|
||||
get(id: string | null): Observable<Client> {
|
||||
const getUrl: string = id === null ? url : `${url}/${id}`;
|
||||
return this.http
|
||||
.get<Client>(getUrl)
|
||||
.pipe(catchError(this.log.handleError(serviceName, `get id=${id}`))) as Observable<Client>;
|
||||
get(id: Signal<string | null>) {
|
||||
return httpResource<Client>(() => {
|
||||
const getUrl: string = id() === null ? url : `${url}/${id()}`;
|
||||
return getUrl;
|
||||
});
|
||||
}
|
||||
|
||||
list(): Observable<Client[]> {
|
||||
return this.http
|
||||
.get<Client[]>(`${url}/list`)
|
||||
.pipe(catchError(this.log.handleError(serviceName, 'list'))) as Observable<Client[]>;
|
||||
list() {
|
||||
return httpResource<Client[]>(() => `${url}/list`);
|
||||
}
|
||||
|
||||
update(client: Client): Observable<Client> {
|
||||
return this.http
|
||||
.put<Client>(`${url}/${client.id}`, client)
|
||||
.pipe(catchError(this.log.handleError(serviceName, 'update'))) as Observable<Client>;
|
||||
.pipe(catchError(this.log.handleError('ClientService', 'update'))) as Observable<Client>;
|
||||
}
|
||||
|
||||
delete(id: string): Observable<Client> {
|
||||
return this.http
|
||||
.delete<Client>(`${url}/${id}`)
|
||||
.pipe(catchError(this.log.handleError(serviceName, 'delete'))) as Observable<Client>;
|
||||
.pipe(catchError(this.log.handleError('ClientService', 'delete'))) as Observable<Client>;
|
||||
}
|
||||
}
|
||||
@@ -1,73 +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 } from 'rxjs/operators';
|
||||
|
||||
import { ClosingStockItem } from './closing-stock-item';
|
||||
|
||||
/** Simple sort comparator for example ID/Name columns (for client-side sorting). */
|
||||
const compare = (a: string | number, b: string | number, isAsc: boolean) => (a < b ? -1 : 1) * (isAsc ? 1 : -1);
|
||||
export class ClosingStockDataSource extends DataSource<ClosingStockItem> {
|
||||
constructor(
|
||||
public data: ClosingStockItem[],
|
||||
private paginator?: MatPaginator,
|
||||
private sort?: MatSort,
|
||||
) {
|
||||
super();
|
||||
}
|
||||
|
||||
connect(): Observable<ClosingStockItem[]> {
|
||||
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);
|
||||
}
|
||||
|
||||
// Set the paginators length
|
||||
if (this.paginator) {
|
||||
this.paginator.length = this.data.length;
|
||||
}
|
||||
|
||||
return merge(observableOf(this.data), ...dataMutations).pipe(
|
||||
map(() => this.getPagedData(this.getSortedData([...this.data]))),
|
||||
);
|
||||
}
|
||||
|
||||
disconnect() {}
|
||||
|
||||
private getPagedData(data: ClosingStockItem[]) {
|
||||
if (this.paginator === undefined) {
|
||||
return data;
|
||||
}
|
||||
const startIndex = this.paginator.pageIndex * this.paginator.pageSize;
|
||||
return data.splice(startIndex, this.paginator.pageSize);
|
||||
}
|
||||
|
||||
private getSortedData(data: ClosingStockItem[]) {
|
||||
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 'product':
|
||||
return compare(`${a.group} - ${a.product}`, `${b.group} - ${b.product}`, isAsc);
|
||||
case 'quantity':
|
||||
return compare(+a.quantity, +b.quantity, isAsc);
|
||||
case 'amount':
|
||||
return compare(+a.amount, +b.amount, isAsc);
|
||||
default:
|
||||
return 0;
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -1,18 +1,18 @@
|
||||
<h2 class="row-container space-between">
|
||||
<span>Closing Stock</span>
|
||||
@if (dataSource.data.length) {
|
||||
@if ((this.info().items ?? []).length) {
|
||||
<button mat-icon-button (click)="exportCsv()">
|
||||
<mat-icon>save_alt</mat-icon>
|
||||
</button>
|
||||
}
|
||||
</h2>
|
||||
|
||||
<form [formGroup]="form" class="flex-col">
|
||||
<form class="flex-col" [formRoot]="form">
|
||||
<div class="row-container">
|
||||
<mat-form-field class="flex-auto basis-2-5">
|
||||
<mat-label>Department</mat-label>
|
||||
<mat-select formControlName="costCentre" name="costCentre">
|
||||
@for (at of costCentres; track at) {
|
||||
<mat-select [formField]="form.costCentre">
|
||||
@for (at of costCentres(); track at) {
|
||||
<mat-option [value]="at.id">
|
||||
{{ at.name }}
|
||||
</mat-option>
|
||||
@@ -21,96 +21,98 @@
|
||||
</mat-form-field>
|
||||
<mat-form-field class="flex-auto basis-2-5">
|
||||
<mat-label>Date</mat-label>
|
||||
<input matInput #dateElement [matDatepicker]="dateInput" formControlName="date" autocomplete="off" />
|
||||
<input matInput #dateElement [matDatepicker]="dateInput" [formField]="form.date" autocomplete="off" />
|
||||
<mat-datepicker-toggle matSuffix [for]="dateInput"></mat-datepicker-toggle>
|
||||
<mat-datepicker #dateInput></mat-datepicker>
|
||||
</mat-form-field>
|
||||
<button mat-raised-button color="primary" (click)="show()" class="flex-auto basis-1-5">Show</button>
|
||||
</div>
|
||||
<mat-table #table [dataSource]="dataSource" matSort aria-label="Elements" formArrayName="stocks">
|
||||
<!-- Product Column -->
|
||||
<ng-container matColumnDef="product" class="first">
|
||||
<mat-header-cell *matHeaderCellDef mat-sort-header class="first">Product</mat-header-cell>
|
||||
<mat-cell *matCellDef="let row">{{ row.product.name }}</mat-cell>
|
||||
</ng-container>
|
||||
@if (infoResource.isLoading()) {
|
||||
<app-skeleton-loader type="table" [count]="1" />
|
||||
} @else if (infoResource.error()) {
|
||||
<app-error-state [message]="'Failed to load data.'" (retryAction)="infoResource.reload()" />
|
||||
} @else {
|
||||
<mat-table #table [dataSource]="dataSource()" matSort (matSortChange)="sortData($event)">
|
||||
<!-- Product Column -->
|
||||
<ng-container matColumnDef="product" class="first">
|
||||
<mat-header-cell *matHeaderCellDef mat-sort-header class="first">Product</mat-header-cell>
|
||||
<mat-cell *matCellDef="let row">{{ row.product.name }}</mat-cell>
|
||||
</ng-container>
|
||||
|
||||
<!-- Group Column -->
|
||||
<ng-container matColumnDef="group">
|
||||
<mat-header-cell *matHeaderCellDef class="middle">Group</mat-header-cell>
|
||||
<mat-cell *matCellDef="let row" class="middle">{{ row.group }}</mat-cell>
|
||||
</ng-container>
|
||||
<!-- Group Column -->
|
||||
<ng-container matColumnDef="group">
|
||||
<mat-header-cell *matHeaderCellDef class="middle">Group</mat-header-cell>
|
||||
<mat-cell *matCellDef="let row" class="middle">{{ row.group }}</mat-cell>
|
||||
</ng-container>
|
||||
|
||||
<!-- Quantity Column -->
|
||||
<ng-container matColumnDef="quantity">
|
||||
<mat-header-cell *matHeaderCellDef mat-sort-header class="right middle">Closing Stock</mat-header-cell>
|
||||
<mat-cell *matCellDef="let row" class="right middle">{{ row.quantity | number: '0.2-2' }}</mat-cell>
|
||||
</ng-container>
|
||||
<!-- Quantity Column -->
|
||||
<ng-container matColumnDef="quantity">
|
||||
<mat-header-cell *matHeaderCellDef mat-sort-header class="right middle">Closing Stock</mat-header-cell>
|
||||
<mat-cell *matCellDef="let row" class="right middle">{{ row.quantity | number: '0.2-2' }}</mat-cell>
|
||||
</ng-container>
|
||||
|
||||
<!-- Physical Column -->
|
||||
<ng-container matColumnDef="physical">
|
||||
<mat-header-cell *matHeaderCellDef class="middle">Physical Stock</mat-header-cell>
|
||||
<mat-cell *matCellDef="let row; let i = index" [formGroupName]="i" class="middle">
|
||||
<mat-form-field class="flex-auto">
|
||||
<mat-label>Physical</mat-label>
|
||||
<input matInput type="number" formControlName="physical" (change)="updatePhysical($event, row)" />
|
||||
</mat-form-field>
|
||||
</mat-cell>
|
||||
</ng-container>
|
||||
<!-- Physical Column -->
|
||||
<ng-container matColumnDef="physical">
|
||||
<mat-header-cell *matHeaderCellDef class="middle">Physical Stock</mat-header-cell>
|
||||
<mat-cell *matCellDef="let row; let i = index" class="middle">
|
||||
<mat-form-field class="flex-auto">
|
||||
<mat-label>Physical</mat-label>
|
||||
<input matInput type="number" formControlName="physical" (change)="updatePhysical($event, row)" />
|
||||
</mat-form-field>
|
||||
</mat-cell>
|
||||
</ng-container>
|
||||
|
||||
<!-- Variance Column -->
|
||||
<ng-container matColumnDef="variance" class="middle">
|
||||
<mat-header-cell *matHeaderCellDef class="right">Variance</mat-header-cell>
|
||||
<mat-cell *matCellDef="let row" class="right middle">{{
|
||||
row.quantity - row.physical | number: '0.2-2'
|
||||
}}</mat-cell>
|
||||
</ng-container>
|
||||
<!-- Variance Column -->
|
||||
<ng-container matColumnDef="variance" class="middle">
|
||||
<mat-header-cell *matHeaderCellDef class="right">Variance</mat-header-cell>
|
||||
<mat-cell *matCellDef="let row" class="right middle">{{
|
||||
row.quantity - row.physical | number: '0.2-2'
|
||||
}}</mat-cell>
|
||||
</ng-container>
|
||||
|
||||
<!-- Department Column -->
|
||||
<ng-container matColumnDef="department">
|
||||
<mat-header-cell *matHeaderCellDef class="middle">Department</mat-header-cell>
|
||||
<mat-cell *matCellDef="let row; let i = index" [formGroupName]="i" class="middle">
|
||||
<mat-form-field class="flex-auto">
|
||||
<mat-label>Department</mat-label>
|
||||
<mat-select
|
||||
formControlName="costCentre"
|
||||
name="costCentre"
|
||||
(selectionChange)="updateDepartment($event.value, row)"
|
||||
>
|
||||
@for (at of costCentres; track at) {
|
||||
<mat-option [value]="at.id">
|
||||
{{ at.name }}
|
||||
</mat-option>
|
||||
}
|
||||
</mat-select>
|
||||
</mat-form-field>
|
||||
</mat-cell>
|
||||
</ng-container>
|
||||
<!-- Department Column -->
|
||||
<ng-container matColumnDef="department">
|
||||
<mat-header-cell *matHeaderCellDef class="middle">Department</mat-header-cell>
|
||||
<mat-cell *matCellDef="let row; let i = index" class="middle">
|
||||
<mat-form-field class="flex-auto">
|
||||
<mat-label>Department</mat-label>
|
||||
<mat-select [formField]="form.costCentre" (selectionChange)="updateDepartment($event.value, row)">
|
||||
@for (at of costCentres(); track at) {
|
||||
<mat-option [value]="at.id">
|
||||
{{ at.name }}
|
||||
</mat-option>
|
||||
}
|
||||
</mat-select>
|
||||
</mat-form-field>
|
||||
</mat-cell>
|
||||
</ng-container>
|
||||
|
||||
<!-- Amount Column -->
|
||||
<ng-container matColumnDef="amount" class="last">
|
||||
<mat-header-cell *matHeaderCellDef mat-sort-header class="right last">Amount</mat-header-cell>
|
||||
<mat-cell *matCellDef="let row" class="right">{{ row.amount | currency: 'INR' }}</mat-cell>
|
||||
</ng-container>
|
||||
<!-- Amount Column -->
|
||||
<ng-container matColumnDef="amount" class="last">
|
||||
<mat-header-cell *matHeaderCellDef mat-sort-header class="right last">Amount</mat-header-cell>
|
||||
<mat-cell *matCellDef="let row" class="right">{{ row.amount | currency: 'INR' }}</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, 300, 5000]"
|
||||
>
|
||||
</mat-paginator>
|
||||
<mat-paginator
|
||||
(page)="handlePageEvent($event)"
|
||||
[length]="(this.info().items ?? []).length"
|
||||
[pageIndex]="pageIndex()"
|
||||
[pageSize]="pageSize()"
|
||||
[pageSizeOptions]="[25, 50, 100, 250, 300, 5000]"
|
||||
>
|
||||
</mat-paginator>
|
||||
}
|
||||
</form>
|
||||
|
||||
<div class="row-container">
|
||||
<button mat-raised-button color="primary" (click)="save()" [disabled]="form.pristine">Save</button>
|
||||
<button mat-raised-button color="primary" (click)="save()" [disabled]="!$any(form).dirty">Save</button>
|
||||
@if (canDelete()) {
|
||||
<button mat-raised-button (click)="post()" [disabled]="info.posted || !auth.allowed('post-vouchers')">
|
||||
{{ info.posted ? 'Posted' : 'Post' }}
|
||||
<button mat-raised-button (click)="post()" [disabled]="info().posted || !auth.allowed('post-vouchers')">
|
||||
{{ info().posted ? 'Posted' : 'Post' }}
|
||||
</button>
|
||||
<button mat-raised-button color="warn" (click)="confirmDelete()" [disabled]="!canSave()">Delete</button>
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { CurrencyPipe, DecimalPipe } from '@angular/common';
|
||||
import { Component, ElementRef, HostListener, inject, OnInit, ViewChild } from '@angular/core';
|
||||
import { FormArray, FormControl, FormGroup, ReactiveFormsModule } from '@angular/forms';
|
||||
import { Component, HostListener, inject, computed, input, linkedSignal, signal, afterNextRender } from '@angular/core';
|
||||
import { form as createForm, FormField, FormRoot } from '@angular/forms/signals';
|
||||
import { MatButtonModule } from '@angular/material/button';
|
||||
import { MatOptionModule } from '@angular/material/core';
|
||||
import { MatDatepickerModule } from '@angular/material/datepicker';
|
||||
@@ -8,31 +8,40 @@ import { MatDialog } from '@angular/material/dialog';
|
||||
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 { MatPaginatorModule, PageEvent } from '@angular/material/paginator';
|
||||
import { MatSelectChange, MatSelectModule } from '@angular/material/select';
|
||||
import { MatSnackBar } from '@angular/material/snack-bar';
|
||||
import { MatSort, MatSortModule } from '@angular/material/sort';
|
||||
import { MatSortModule, Sort } from '@angular/material/sort';
|
||||
import { MatTableModule } from '@angular/material/table';
|
||||
import { ActivatedRoute, Router } from '@angular/router';
|
||||
import { Router } from '@angular/router';
|
||||
import moment from 'moment';
|
||||
|
||||
import { AuthService } from '../auth/auth.service';
|
||||
import { CostCentre } from '../core/cost-centre';
|
||||
import { User } from '../core/user';
|
||||
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 { ToCsvService } from '../shared/to-csv.service';
|
||||
import { ClosingStock } from './closing-stock';
|
||||
import { ClosingStockDataSource } from './closing-stock-datasource';
|
||||
import { ClosingStockItem } from './closing-stock-item';
|
||||
import { ClosingStockService } from './closing-stock.service';
|
||||
|
||||
export interface ClosingStockFormData {
|
||||
date: moment.Moment;
|
||||
costCentre: CostCentre | null;
|
||||
stocks: { physical: number; costCentre: string | undefined }[];
|
||||
}
|
||||
|
||||
@Component({
|
||||
selector: 'app-closing-stock',
|
||||
templateUrl: './closing-stock.component.html',
|
||||
styleUrls: ['./closing-stock.component.css'],
|
||||
imports: [
|
||||
MatIconModule,
|
||||
ReactiveFormsModule,
|
||||
FormField,
|
||||
FormRoot,
|
||||
MatFormFieldModule,
|
||||
MatSelectModule,
|
||||
MatOptionModule,
|
||||
@@ -44,75 +53,92 @@ import { ClosingStockService } from './closing-stock.service';
|
||||
MatPaginatorModule,
|
||||
DecimalPipe,
|
||||
CurrencyPipe,
|
||||
|
||||
SkeletonLoaderComponent,
|
||||
ErrorStateComponent,
|
||||
],
|
||||
})
|
||||
export class ClosingStockComponent implements OnInit {
|
||||
private route = inject(ActivatedRoute);
|
||||
export class ClosingStockComponent {
|
||||
private router = inject(Router);
|
||||
private toCsv = inject(ToCsvService);
|
||||
private dialog = inject(MatDialog);
|
||||
private snackBar = inject(MatSnackBar);
|
||||
auth = inject(AuthService);
|
||||
protected auth = inject(AuthService);
|
||||
private ser = inject(ClosingStockService);
|
||||
private costCentreSer = inject(CostCentreService);
|
||||
pageSize = signal(50);
|
||||
pageIndex = signal(0);
|
||||
sortActive = signal('');
|
||||
sortDirection = signal('');
|
||||
id = input(null, { transform: (v: string | null | undefined) => v ?? null });
|
||||
d = input(null, { transform: (v: string | null | undefined) => v ?? null });
|
||||
startDate = input(null, { transform: (v: string | null | undefined) => v ?? null });
|
||||
finishDate = input(null, { transform: (v: string | null | undefined) => v ?? null });
|
||||
infoResource = this.ser.list(this.id, this.d);
|
||||
info = computed(() => this.infoResource.value() ?? new ClosingStock());
|
||||
costCentresResource = this.costCentreSer.list();
|
||||
|
||||
costCentres = computed(() => this.costCentresResource.value() ?? []);
|
||||
|
||||
model = linkedSignal({
|
||||
source: computed(() => ({ info: this.info(), costCentres: this.costCentres() })),
|
||||
computation: ({ info, costCentres }): ClosingStockFormData => ({
|
||||
date: moment(info.date, 'DD-MMM-YYYY'),
|
||||
costCentre: costCentres.find((c) => c.id === info.costCentre?.id) || null,
|
||||
stocks: info.items.map((x) => ({
|
||||
physical: x.physical,
|
||||
costCentre: x.costCentre?.id,
|
||||
})),
|
||||
}),
|
||||
});
|
||||
|
||||
form = createForm(this.model);
|
||||
|
||||
sortedList = computed(() => {
|
||||
const data = this.info().items ?? [];
|
||||
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 'product':
|
||||
return compare(a.product?.name ?? '', b.product?.name ?? '', isAsc);
|
||||
case 'group':
|
||||
return compare(a.group ?? '', b.group ?? '', isAsc);
|
||||
case 'quantity':
|
||||
return compare(a.quantity ?? 0, b.quantity ?? 0, isAsc);
|
||||
case 'amount':
|
||||
return compare(a.amount ?? 0, b.amount ?? 0, 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);
|
||||
});
|
||||
|
||||
@ViewChild(MatPaginator, { static: true }) paginator!: MatPaginator;
|
||||
@ViewChild(MatSort, { static: true }) sort!: MatSort;
|
||||
@ViewChild('dateElement', { static: true }) dateElement!: ElementRef<HTMLInputElement>;
|
||||
info: ClosingStock = new ClosingStock();
|
||||
dataSource: ClosingStockDataSource = new ClosingStockDataSource(this.info.items, this.paginator, this.sort);
|
||||
form: FormGroup<{
|
||||
date: FormControl<moment.Moment>;
|
||||
costCentre: FormControl<string | null>;
|
||||
stocks: FormArray<
|
||||
FormGroup<{
|
||||
physical: FormControl<number>;
|
||||
costCentre: FormControl<string | undefined>;
|
||||
}>
|
||||
>;
|
||||
}>;
|
||||
/** Columns displayed in the table. Columns IDs can be added, removed, or reordered. */
|
||||
|
||||
displayedColumns = ['product', 'group', 'quantity', 'physical', 'variance', 'department', 'amount'];
|
||||
|
||||
costCentres: CostCentre[];
|
||||
|
||||
@HostListener('window:keydown.f2', ['$event'])
|
||||
focusDate(event: Event) {
|
||||
event.preventDefault();
|
||||
this.dateElement.nativeElement.focus();
|
||||
this.dateElement.nativeElement.select();
|
||||
this.form().focusBoundControl();
|
||||
}
|
||||
|
||||
constructor() {
|
||||
this.costCentres = [];
|
||||
this.form = new FormGroup({
|
||||
date: new FormControl(moment(new Date()), { nonNullable: true }),
|
||||
costCentre: new FormControl<string | null>(null),
|
||||
stocks: new FormArray<FormGroup<{ physical: FormControl<number>; costCentre: FormControl<string | undefined> }>>(
|
||||
[],
|
||||
),
|
||||
});
|
||||
}
|
||||
|
||||
ngOnInit() {
|
||||
this.route.data.subscribe((value) => {
|
||||
const data = value as { info: ClosingStock; costCentres: CostCentre[] };
|
||||
this.info = data.info;
|
||||
this.costCentres = data.costCentres;
|
||||
this.form.patchValue({
|
||||
date: moment(this.info.date, 'DD-MMM-YYYY'),
|
||||
costCentre: this.info.costCentre.id,
|
||||
});
|
||||
this.form.controls.stocks.clear();
|
||||
this.info.items.forEach((x) =>
|
||||
this.form.controls.stocks.push(
|
||||
new FormGroup({
|
||||
physical: new FormControl(x.physical, { nonNullable: true }),
|
||||
costCentre: new FormControl(x.costCentre?.id, { nonNullable: true }),
|
||||
}),
|
||||
),
|
||||
);
|
||||
this.dataSource = new ClosingStockDataSource(this.info.items, this.paginator, this.sort);
|
||||
afterNextRender(() => {
|
||||
this.form().focusBoundControl();
|
||||
});
|
||||
}
|
||||
|
||||
@@ -137,25 +163,23 @@ export class ClosingStockComponent implements OnInit {
|
||||
}
|
||||
|
||||
getClosingStock(): ClosingStock {
|
||||
const formModel = this.form.value;
|
||||
this.info.date = moment(formModel.date).format('DD-MMM-YYYY');
|
||||
const array = this.form.controls.stocks;
|
||||
this.info.items.forEach((item, index) => {
|
||||
item.physical = +(array.controls[index].value.physical ?? '');
|
||||
item.costCentre =
|
||||
array.controls[index].value.costCentre == null
|
||||
? undefined
|
||||
: new CostCentre({ id: array.controls[index].value.costCentre });
|
||||
const formModel = this.model();
|
||||
const currentInfo = this.info();
|
||||
currentInfo.date = formModel.date.format('DD-MMM-YYYY');
|
||||
const array = formModel.stocks;
|
||||
currentInfo.items.forEach((item, index) => {
|
||||
item.physical = array[index].physical ?? 0;
|
||||
item.costCentre = array[index].costCentre == null ? undefined : new CostCentre({ id: array[index].costCentre });
|
||||
});
|
||||
return this.info;
|
||||
return currentInfo;
|
||||
}
|
||||
|
||||
getInfo(): ClosingStock {
|
||||
const formModel = this.form.value;
|
||||
const formModel = this.model();
|
||||
|
||||
return new ClosingStock({
|
||||
date: moment(formModel.date).format('DD-MMM-YYYY'),
|
||||
costCentre: new CostCentre({ id: formModel.costCentre ?? '' }),
|
||||
date: formModel.date.format('DD-MMM-YYYY'),
|
||||
costCentre: new CostCentre({ id: formModel.costCentre?.id ?? '' }),
|
||||
});
|
||||
}
|
||||
|
||||
@@ -167,7 +191,7 @@ export class ClosingStockComponent implements OnInit {
|
||||
Amount: 'amount',
|
||||
};
|
||||
|
||||
const d = JSON.parse(JSON.stringify(this.dataSource.data)).map((x: ClosingStockItem) => ({
|
||||
const d = JSON.parse(JSON.stringify(this.dataSource())).map((x: ClosingStockItem) => ({
|
||||
product: x.product.name,
|
||||
group: x.group,
|
||||
quantity: x.quantity,
|
||||
@@ -193,21 +217,21 @@ export class ClosingStockComponent implements OnInit {
|
||||
}
|
||||
|
||||
canDelete() {
|
||||
return this.info.items.find((x) => !!x.id) !== undefined;
|
||||
return this.info().items.find((x) => !!x.id) !== undefined;
|
||||
}
|
||||
|
||||
canSave() {
|
||||
if (this.info.items.find((x) => !!x.id) !== undefined) {
|
||||
if (this.info().items.find((x) => !!x.id) !== undefined) {
|
||||
return true;
|
||||
}
|
||||
if (this.info.posted && this.auth.allowed('edit-posted-vouchers')) {
|
||||
if (this.info().posted && this.auth.allowed('edit-posted-vouchers')) {
|
||||
return true;
|
||||
}
|
||||
return this.info.user.id === (this.auth.user as User).id || this.auth.allowed("edit-other-user's-vouchers");
|
||||
return this.info().user.id === (this.auth.user as User).id || this.auth.allowed("edit-other-user's-vouchers");
|
||||
}
|
||||
|
||||
post() {
|
||||
this.ser.post(this.info.date, this.info.costCentre.id as string).subscribe({
|
||||
this.ser.post(this.info().date, this.info().costCentre.id as string).subscribe({
|
||||
next: () => {
|
||||
this.snackBar.open('Voucher Posted', 'Success');
|
||||
},
|
||||
@@ -218,7 +242,7 @@ export class ClosingStockComponent implements OnInit {
|
||||
}
|
||||
|
||||
delete() {
|
||||
this.ser.delete(this.info.date, this.info.costCentre.id as string).subscribe({
|
||||
this.ser.delete(this.info().date, this.info().costCentre.id as string).subscribe({
|
||||
next: () => {
|
||||
this.snackBar.open('', 'Success');
|
||||
this.router.navigate(['/closing-stock'], { replaceUrl: true });
|
||||
@@ -244,4 +268,16 @@ export class ClosingStockComponent implements OnInit {
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
handlePageEvent(e: PageEvent) {
|
||||
this.pageSize.set(e.pageSize);
|
||||
this.pageIndex.set(e.pageIndex);
|
||||
}
|
||||
|
||||
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 { ClosingStock } from './closing-stock';
|
||||
import { closingStockResolver } from './closing-stock.resolver';
|
||||
|
||||
describe('closingStockResolver', () => {
|
||||
const executeResolver: ResolveFn<ClosingStock> = (...resolverParameters) =>
|
||||
TestBed.runInInjectionContext(() => closingStockResolver(...resolverParameters));
|
||||
|
||||
beforeEach(() => {
|
||||
TestBed.configureTestingModule({});
|
||||
});
|
||||
|
||||
it('should be created', () => {
|
||||
expect(executeResolver).toBeTruthy();
|
||||
});
|
||||
});
|
||||
@@ -1,11 +0,0 @@
|
||||
import { inject } from '@angular/core';
|
||||
import { ResolveFn } from '@angular/router';
|
||||
|
||||
import { ClosingStock } from './closing-stock';
|
||||
import { ClosingStockService } from './closing-stock.service';
|
||||
|
||||
export const closingStockResolver: ResolveFn<ClosingStock> = (route) => {
|
||||
const date = route.paramMap.get('date');
|
||||
const costCentre = route.queryParamMap.get('d') || null;
|
||||
return inject(ClosingStockService).list(date, costCentre);
|
||||
};
|
||||
@@ -1,9 +1,7 @@
|
||||
import { Routes } from '@angular/router';
|
||||
|
||||
import { authGuard } from '../auth/auth-guard.service';
|
||||
import { costCentreListResolver } from '../cost-centre/cost-centre-list.resolver';
|
||||
import { ClosingStockComponent } from './closing-stock.component';
|
||||
import { closingStockResolver } from './closing-stock.resolver';
|
||||
|
||||
export const routes: Routes = [
|
||||
{
|
||||
@@ -13,10 +11,6 @@ export const routes: Routes = [
|
||||
data: {
|
||||
permission: 'Closing Stock',
|
||||
},
|
||||
resolve: {
|
||||
info: closingStockResolver,
|
||||
costCentres: costCentreListResolver,
|
||||
},
|
||||
runGuardsAndResolvers: 'always',
|
||||
},
|
||||
{
|
||||
@@ -26,10 +20,6 @@ export const routes: Routes = [
|
||||
data: {
|
||||
permission: 'Closing Stock',
|
||||
},
|
||||
resolve: {
|
||||
info: closingStockResolver,
|
||||
costCentres: costCentreListResolver,
|
||||
},
|
||||
runGuardsAndResolvers: 'always',
|
||||
},
|
||||
];
|
||||
@@ -1,5 +1,5 @@
|
||||
import { HttpClient, HttpParams } from '@angular/common/http';
|
||||
import { inject, Injectable } from '@angular/core';
|
||||
import { HttpClient, httpResource, HttpParams } from '@angular/common/http';
|
||||
import { inject, Injectable, Signal } from '@angular/core';
|
||||
import { Observable } from 'rxjs/internal/Observable';
|
||||
import { catchError } from 'rxjs/operators';
|
||||
|
||||
@@ -7,8 +7,6 @@ import { ErrorLoggerService } from '../core/error-logger.service';
|
||||
import { ClosingStock } from './closing-stock';
|
||||
|
||||
const url = '/api/closing-stock';
|
||||
const serviceName = 'ClosingStockService';
|
||||
|
||||
@Injectable({
|
||||
providedIn: 'root',
|
||||
})
|
||||
@@ -16,34 +14,33 @@ export class ClosingStockService {
|
||||
private http = inject(HttpClient);
|
||||
private log = inject(ErrorLoggerService);
|
||||
|
||||
list(date: string | null, costCentre: string | null): Observable<ClosingStock> {
|
||||
const listUrl = date === null ? url : `${url}/${date}`;
|
||||
const options = { params: new HttpParams() };
|
||||
if (costCentre !== null) {
|
||||
options.params = options.params.set('d', costCentre);
|
||||
}
|
||||
return this.http
|
||||
.get<ClosingStock>(listUrl, options)
|
||||
.pipe(catchError(this.log.handleError(serviceName, 'list'))) as Observable<ClosingStock>;
|
||||
list(date: Signal<string | null>, costCentre: Signal<string | null>) {
|
||||
return httpResource<ClosingStock>(() => {
|
||||
const listUrl = date() === null ? url : `${url}/${date()}`;
|
||||
const params: Record<string, string> = {};
|
||||
const costCentre_val = costCentre();
|
||||
if (costCentre_val !== null) params['d'] = costCentre_val;
|
||||
return { url: listUrl, params };
|
||||
});
|
||||
}
|
||||
|
||||
save(closingStock: ClosingStock): Observable<ClosingStock> {
|
||||
return this.http
|
||||
.post<ClosingStock>(url, closingStock)
|
||||
.pipe(catchError(this.log.handleError(serviceName, 'save'))) as Observable<ClosingStock>;
|
||||
.pipe(catchError(this.log.handleError('ClosingStockService', 'save'))) as Observable<ClosingStock>;
|
||||
}
|
||||
|
||||
post(date: string, costCentre: string): Observable<ClosingStock> {
|
||||
const options = { params: new HttpParams().set('d', costCentre) };
|
||||
return this.http
|
||||
.post<ClosingStock>(`${url}/${date}`, {}, options)
|
||||
.pipe(catchError(this.log.handleError(serviceName, 'Post Voucher'))) as Observable<ClosingStock>;
|
||||
.pipe(catchError(this.log.handleError('ClosingStockService', 'Post Voucher'))) as Observable<ClosingStock>;
|
||||
}
|
||||
|
||||
delete(date: string, costCentre: string): Observable<ClosingStock> {
|
||||
const options = { params: new HttpParams().set('d', costCentre) };
|
||||
return this.http
|
||||
.delete<ClosingStock>(`${url}/${date}`, options)
|
||||
.pipe(catchError(this.log.handleError(serviceName, 'Delete Voucher'))) as Observable<ClosingStock>;
|
||||
.pipe(catchError(this.log.handleError('ClosingStockService', 'Delete Voucher'))) as Observable<ClosingStock>;
|
||||
}
|
||||
}
|
||||
@@ -1,5 +1,5 @@
|
||||
import { HttpClient, HttpParams } from '@angular/common/http';
|
||||
import { inject, Injectable } from '@angular/core';
|
||||
import { HttpClient, httpResource } from '@angular/common/http';
|
||||
import { inject, Injectable, Signal } from '@angular/core';
|
||||
import { Observable } from 'rxjs/internal/Observable';
|
||||
import { catchError } from 'rxjs/operators';
|
||||
|
||||
@@ -8,50 +8,40 @@ import { AccountBalance } from './account-balance';
|
||||
import { ErrorLoggerService } from './error-logger.service';
|
||||
|
||||
const url = '/api/accounts';
|
||||
const serviceName = 'AccountService';
|
||||
|
||||
@Injectable({ providedIn: 'root' })
|
||||
export class AccountService {
|
||||
private http = inject(HttpClient);
|
||||
private log = inject(ErrorLoggerService);
|
||||
|
||||
get(id: string | null): Observable<Account> {
|
||||
const getUrl: string = id === null ? `${url}` : `${url}/${id}`;
|
||||
return this.http
|
||||
.get<Account>(getUrl)
|
||||
.pipe(catchError(this.log.handleError(serviceName, `get id=${id}`))) as Observable<Account>;
|
||||
get(id: Signal<string | null>) {
|
||||
return httpResource<Account>(() => {
|
||||
const getUrl: string = id() === null ? `${url}` : `${url}/${id()}`;
|
||||
return getUrl;
|
||||
});
|
||||
}
|
||||
|
||||
list(): Observable<Account[]> {
|
||||
return this.http
|
||||
.get<Account[]>(`${url}/list`)
|
||||
.pipe(catchError(this.log.handleError(serviceName, 'list'))) as Observable<Account[]>;
|
||||
list() {
|
||||
return httpResource<Account[]>(() => `${url}/list`);
|
||||
}
|
||||
|
||||
paymentAutocomplete(query: string): Observable<Account[]> {
|
||||
const options = { params: new HttpParams().set('q', query).set('t', '1') };
|
||||
return this.http
|
||||
.get<Account[]>(`${url}/query`, options)
|
||||
.pipe(catchError(this.log.handleError(serviceName, 'list'))) as Observable<Account[]>;
|
||||
paymentAutocomplete(query: Signal<string>) {
|
||||
return httpResource<Account[]>(() => ({ url: `${url}/query`, params: { q: query(), t: '1' } }));
|
||||
}
|
||||
|
||||
receiptAutocomplete(query: string): Observable<Account[]> {
|
||||
const options = { params: new HttpParams().set('q', query).set('t', '1') };
|
||||
return this.http
|
||||
.get<Account[]>(`${url}/query`, options)
|
||||
.pipe(catchError(this.log.handleError(serviceName, 'list'))) as Observable<Account[]>;
|
||||
receiptAutocomplete(query: Signal<string>) {
|
||||
return httpResource<Account[]>(() => ({ url: `${url}/query`, params: { q: query(), t: '1' } }));
|
||||
}
|
||||
|
||||
save(account: Account): Observable<null> {
|
||||
return this.http
|
||||
.post<null>(`${url}`, account)
|
||||
.pipe(catchError(this.log.handleError(serviceName, 'save'))) as Observable<null>;
|
||||
.pipe(catchError(this.log.handleError('AccountService', 'save'))) as Observable<null>;
|
||||
}
|
||||
|
||||
update(account: Account): Observable<null> {
|
||||
return this.http
|
||||
.put<null>(`${url}/${account.id}`, account)
|
||||
.pipe(catchError(this.log.handleError(serviceName, 'update'))) as Observable<null>;
|
||||
.pipe(catchError(this.log.handleError('AccountService', 'update'))) as Observable<null>;
|
||||
}
|
||||
|
||||
saveOrUpdate(account: Account): Observable<null> {
|
||||
@@ -64,20 +54,22 @@ export class AccountService {
|
||||
delete(id: string): Observable<Account> {
|
||||
return this.http
|
||||
.delete<Account>(`${url}/${id}`)
|
||||
.pipe(catchError(this.log.handleError(serviceName, 'delete'))) as Observable<Account>;
|
||||
.pipe(catchError(this.log.handleError('AccountService', 'delete'))) as Observable<Account>;
|
||||
}
|
||||
|
||||
autocomplete(query: string): Observable<Account[]> {
|
||||
const options = { params: new HttpParams().set('q', query) };
|
||||
return this.http
|
||||
.get<Account[]>(`${url}/query`, options)
|
||||
.pipe(catchError(this.log.handleError(serviceName, 'autocomplete'))) as Observable<Account[]>;
|
||||
autocomplete(query: Signal<string | null>) {
|
||||
return httpResource<Account[]>(() => {
|
||||
const q = query();
|
||||
return q ? `${url}/query?q=${encodeURIComponent(q)}` : undefined;
|
||||
});
|
||||
}
|
||||
|
||||
balance(id: string, date: string): Observable<AccountBalance> {
|
||||
const options = { params: new HttpParams().set('d', date) };
|
||||
return this.http
|
||||
.get<AccountBalance>(`${url}/${id}/balance`, options)
|
||||
.pipe(catchError(this.log.handleError(serviceName, 'balance'))) as Observable<AccountBalance>;
|
||||
balance(id: Signal<string | null>, date: Signal<string | null>) {
|
||||
return httpResource<AccountBalance>(() => {
|
||||
const accId = id();
|
||||
const d = date();
|
||||
if (!accId || !d) return undefined;
|
||||
return `${url}/${accId}/balance?d=${encodeURIComponent(d)}`;
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -1,23 +1,21 @@
|
||||
import { HttpClient, HttpParams } from '@angular/common/http';
|
||||
import { inject, Injectable } from '@angular/core';
|
||||
import { Observable } from 'rxjs/internal/Observable';
|
||||
import { catchError } from 'rxjs/operators';
|
||||
import { HttpClient, httpResource, HttpParams } from '@angular/common/http';
|
||||
import { inject, Injectable, Signal } from '@angular/core';
|
||||
|
||||
import { Batch } from './batch';
|
||||
import { ErrorLoggerService } from './error-logger.service';
|
||||
|
||||
const url = '/api/batch';
|
||||
const serviceName = 'BatchService';
|
||||
|
||||
@Injectable({ providedIn: 'root' })
|
||||
export class BatchService {
|
||||
private http = inject(HttpClient);
|
||||
private log = inject(ErrorLoggerService);
|
||||
|
||||
autocomplete(date: string, term: string): Observable<Batch[]> {
|
||||
const options = { params: new HttpParams().set('q', term).set('d', date) };
|
||||
return this.http
|
||||
.get<Batch[]>(url, options)
|
||||
.pipe(catchError(this.log.handleError(serviceName, 'autocomplete'))) as Observable<Batch[]>;
|
||||
autocomplete(date: Signal<string | null>, term: Signal<string | null>) {
|
||||
return httpResource<Batch[]>(() => {
|
||||
const q = term();
|
||||
const d = date();
|
||||
if (!q || !d) return undefined;
|
||||
return { url, params: new HttpParams().set('q', q).set('d', d) };
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
import { HttpInterceptorFn } from '@angular/common/http';
|
||||
import { timer, switchMap } from 'rxjs';
|
||||
|
||||
export const delayInterceptor: HttpInterceptorFn = (req, next) => {
|
||||
// Delay all requests by 1500ms to test loading states
|
||||
return timer(1500).pipe(switchMap(() => next(req)));
|
||||
};
|
||||
@@ -1,6 +1,6 @@
|
||||
<mat-toolbar class="fixed-to-top row-container">
|
||||
<span class=""
|
||||
><a routerLink="/">{{ title }}</a></span
|
||||
><a routerLink="/">{{ title() }}</a></span
|
||||
>
|
||||
<mat-menu #voucherMenu="matMenu">
|
||||
<a mat-menu-item routerLink="/journal">Journal - F7</a>
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { AsyncPipe } from '@angular/common';
|
||||
import { Component, inject, OnInit } from '@angular/core';
|
||||
import { Component, inject, computed } from '@angular/core';
|
||||
import { MatAnchor, MatButton } from '@angular/material/button';
|
||||
import { MatIcon } from '@angular/material/icon';
|
||||
import { MatMenu, MatMenuItem, MatMenuTrigger } from '@angular/material/menu';
|
||||
@@ -16,22 +16,13 @@ import { TitleService } from '../title.service';
|
||||
styleUrls: ['./nav-bar.component.css'],
|
||||
imports: [MatToolbar, RouterLink, MatMenu, MatMenuItem, MatButton, MatMenuTrigger, MatIcon, MatAnchor, AsyncPipe],
|
||||
})
|
||||
export class NavBarComponent implements OnInit {
|
||||
export class NavBarComponent {
|
||||
private router = inject(Router);
|
||||
auth = inject(AuthService);
|
||||
titleService = inject(TitleService);
|
||||
|
||||
title: string;
|
||||
|
||||
constructor() {
|
||||
this.title = environment.title;
|
||||
}
|
||||
|
||||
ngOnInit() {
|
||||
this.titleService.get().subscribe((value) => {
|
||||
this.title = value;
|
||||
});
|
||||
}
|
||||
private titleResource = this.titleService.get();
|
||||
title = computed(() => this.titleResource.value() ?? environment.title);
|
||||
|
||||
logout() {
|
||||
this.auth.logout();
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
import { Product } from './product';
|
||||
|
||||
export class ProductSku {
|
||||
id: string;
|
||||
name: string;
|
||||
@@ -6,6 +8,8 @@ export class ProductSku {
|
||||
fractionUnits: string;
|
||||
|
||||
isRateContracted: boolean;
|
||||
stock?: number;
|
||||
product?: Product;
|
||||
|
||||
public constructor(init?: Partial<ProductSku>) {
|
||||
this.id = '';
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { ProductGroup } from './product-group';
|
||||
|
||||
export class StockKeepingUnit {
|
||||
id: string | null;
|
||||
units: string;
|
||||
fraction: number;
|
||||
productYield: number;
|
||||
@@ -8,6 +9,7 @@ export class StockKeepingUnit {
|
||||
salePrice: number;
|
||||
|
||||
public constructor(init?: Partial<StockKeepingUnit>) {
|
||||
this.id = null;
|
||||
this.units = '';
|
||||
this.fraction = 1;
|
||||
this.productYield = 1;
|
||||
@@ -23,6 +25,7 @@ export class Product {
|
||||
name: string;
|
||||
description: string | undefined;
|
||||
skus: StockKeepingUnit[];
|
||||
unit?: { name: string };
|
||||
fractionUnits: string | undefined;
|
||||
|
||||
isActive: boolean;
|
||||
|
||||
@@ -1,23 +1,12 @@
|
||||
import { HttpClient } from '@angular/common/http';
|
||||
import { inject, Injectable } from '@angular/core';
|
||||
import { Observable } from 'rxjs/internal/Observable';
|
||||
import { catchError } from 'rxjs/operators';
|
||||
|
||||
import { ErrorLoggerService } from './error-logger.service';
|
||||
import { httpResource } from '@angular/common/http';
|
||||
import { Injectable } from '@angular/core';
|
||||
|
||||
const url = '/api/title';
|
||||
const serviceName = 'VoucherService';
|
||||
|
||||
@Injectable({
|
||||
providedIn: 'root',
|
||||
})
|
||||
export class TitleService {
|
||||
private http = inject(HttpClient);
|
||||
private log = inject(ErrorLoggerService);
|
||||
|
||||
get(): Observable<string> {
|
||||
return this.http
|
||||
.get<string>(url)
|
||||
.pipe(catchError(this.log.handleError(serviceName, 'Get Title'))) as Observable<string>;
|
||||
get() {
|
||||
return httpResource<string>(() => url);
|
||||
}
|
||||
}
|
||||
@@ -1,5 +1,5 @@
|
||||
import { HttpClient, HttpParams } from '@angular/common/http';
|
||||
import { inject, Injectable } from '@angular/core';
|
||||
import { HttpClient, httpResource } from '@angular/common/http';
|
||||
import { inject, Injectable, Signal, ResourceRef } from '@angular/core';
|
||||
import { Observable } from 'rxjs/internal/Observable';
|
||||
import { catchError } from 'rxjs/operators';
|
||||
|
||||
@@ -7,8 +7,6 @@ import { ErrorLoggerService } from './error-logger.service';
|
||||
import { Voucher } from './voucher';
|
||||
|
||||
const url = '/api';
|
||||
const serviceName = 'VoucherService';
|
||||
|
||||
@Injectable({
|
||||
providedIn: 'root',
|
||||
})
|
||||
@@ -31,46 +29,64 @@ export class VoucherService {
|
||||
return new Blob([ab], { type: mimeString });
|
||||
}
|
||||
|
||||
get(id: string, type: string): Observable<Voucher> {
|
||||
const endpoint = type.replace(/ /g, '-').toLowerCase();
|
||||
return this.http
|
||||
.get<Voucher>(`${url}/${endpoint}/${id}`)
|
||||
.pipe(catchError(this.log.handleError(serviceName, 'Get Voucher'))) as Observable<Voucher>;
|
||||
get(id: Signal<string | undefined>, type: Signal<string>): ResourceRef<Voucher | undefined> {
|
||||
return httpResource<Voucher>(() => {
|
||||
const i = id();
|
||||
if (!i) return undefined;
|
||||
const endpoint = type().replace(/ /g, '-').toLowerCase();
|
||||
return `${url}/${endpoint}/${i}`;
|
||||
});
|
||||
}
|
||||
|
||||
getOfType(type: string, account: string | null, date: string | null): Observable<Voucher> {
|
||||
const endpoint = type.replace(/ /g, '-').toLowerCase();
|
||||
const options = {
|
||||
params: new HttpParams(),
|
||||
};
|
||||
if (account !== null) {
|
||||
options.params = options.params.set('a', account);
|
||||
}
|
||||
if (date !== null) {
|
||||
options.params = options.params.set('d', date);
|
||||
}
|
||||
return this.http
|
||||
.get<Voucher>(`${url}/${endpoint}`, options)
|
||||
.pipe(catchError(this.log.handleError(serviceName, 'Get Voucher of Type'))) as Observable<Voucher>;
|
||||
getOfType(
|
||||
type: Signal<string>,
|
||||
account: Signal<string | null>,
|
||||
date: Signal<string | null>,
|
||||
): ResourceRef<Voucher | undefined> {
|
||||
return httpResource<Voucher>(() => {
|
||||
const endpoint = type().replace(/ /g, '-').toLowerCase();
|
||||
const params = new URLSearchParams();
|
||||
if (account() !== null) params.set('a', account()!);
|
||||
if (date() !== null) params.set('d', date()!);
|
||||
const p = params.toString();
|
||||
return p ? `${url}/${endpoint}?${p}` : `${url}/${endpoint}`;
|
||||
});
|
||||
}
|
||||
|
||||
getIncentive(date: string): Observable<Voucher> {
|
||||
const options = { params: new HttpParams().set('d', date) };
|
||||
return this.http
|
||||
.get<Voucher>(`${url}/incentive`, options)
|
||||
.pipe(catchError(this.log.handleError(serviceName, 'Get Incentive'))) as Observable<Voucher>;
|
||||
getVoucher(
|
||||
id: Signal<string | null>,
|
||||
type: Signal<string>,
|
||||
account: Signal<string | null>,
|
||||
date: Signal<string | null>,
|
||||
): ResourceRef<Voucher | undefined> {
|
||||
return httpResource<Voucher>(() => {
|
||||
const endpoint = type().replace(/ /g, '-').toLowerCase();
|
||||
const i = id();
|
||||
if (i) {
|
||||
return `${url}/${endpoint}/${i}`;
|
||||
}
|
||||
const params = new URLSearchParams();
|
||||
if (account() !== null) params.set('a', account()!);
|
||||
if (date() !== null) params.set('d', date()!);
|
||||
const p = params.toString();
|
||||
return p ? `${url}/${endpoint}?${p}` : `${url}/${endpoint}`;
|
||||
});
|
||||
}
|
||||
|
||||
getIncentive(date: Signal<string>): ResourceRef<Voucher | undefined> {
|
||||
return httpResource<Voucher>(() => `${url}/incentive?d=${date()}`);
|
||||
}
|
||||
|
||||
post(id: string): Observable<Voucher> {
|
||||
return this.http
|
||||
.post<Voucher>(`${url}/post-voucher/${id}`, {})
|
||||
.pipe(catchError(this.log.handleError(serviceName, 'Post Voucher'))) as Observable<Voucher>;
|
||||
.pipe(catchError(this.log.handleError('VoucherService', 'Post Voucher'))) as Observable<Voucher>;
|
||||
}
|
||||
|
||||
delete(id: string): Observable<Voucher> {
|
||||
return this.http
|
||||
.delete<Voucher>(`${url}/delete/${id}`)
|
||||
.pipe(catchError(this.log.handleError(serviceName, 'Delete Voucher'))) as Observable<Voucher>;
|
||||
.pipe(catchError(this.log.handleError('VoucherService', 'Delete Voucher'))) as Observable<Voucher>;
|
||||
}
|
||||
|
||||
saveOrUpdate(voucher: Voucher): Observable<Voucher> {
|
||||
@@ -93,7 +109,7 @@ export class VoucherService {
|
||||
fd.append('data', JSON.stringify(voucher));
|
||||
return this.http
|
||||
.post<Voucher>(`${url}/${endpoint}`, fd)
|
||||
.pipe(catchError(this.log.handleError(serviceName, 'Save Voucher'))) as Observable<Voucher>;
|
||||
.pipe(catchError(this.log.handleError('VoucherService', 'Save Voucher'))) as Observable<Voucher>;
|
||||
}
|
||||
|
||||
update(voucher: Voucher, endpoint: string): Observable<Voucher> {
|
||||
@@ -108,6 +124,6 @@ export class VoucherService {
|
||||
fd.append('data', JSON.stringify(voucher));
|
||||
return this.http
|
||||
.put<Voucher>(`${url}/${endpoint}/${voucher.id}`, fd)
|
||||
.pipe(catchError(this.log.handleError(serviceName, 'Update Voucher'))) as Observable<Voucher>;
|
||||
.pipe(catchError(this.log.handleError('VoucherService', 'Update Voucher'))) as Observable<Voucher>;
|
||||
}
|
||||
}
|
||||
@@ -1,14 +1,21 @@
|
||||
<h2>Cost Center</h2>
|
||||
|
||||
<form [formGroup]="form" class="flex-col wrapped">
|
||||
<div class="row-container">
|
||||
<mat-form-field class="flex-auto">
|
||||
<mat-label>Name</mat-label>
|
||||
<input matInput #nameElement formControlName="name" (keyup.enter)="save()" />
|
||||
</mat-form-field>
|
||||
</div>
|
||||
</form>
|
||||
@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]="form">
|
||||
<div class="row-container">
|
||||
<mat-form-field class="flex-auto">
|
||||
<mat-label>Name</mat-label>
|
||||
<input matInput [formField]="form.name" (keyup.enter)="save()" />
|
||||
</mat-form-field>
|
||||
</div>
|
||||
</form>
|
||||
|
||||
<div class="row-container">
|
||||
<button mat-raised-button (click)="save()" color="primary">Save</button>
|
||||
</div>
|
||||
<div class="row-container">
|
||||
<button mat-raised-button (click)="save()" color="primary">Save</button>
|
||||
</div>
|
||||
}
|
||||
@@ -1,60 +1,60 @@
|
||||
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 as createForm, FormField, FormRoot } from '@angular/forms/signals';
|
||||
import { MatButtonModule } from '@angular/material/button';
|
||||
import { MatFormFieldModule } from '@angular/material/form-field';
|
||||
import { MatInputModule } from '@angular/material/input';
|
||||
import { MatSnackBar } from '@angular/material/snack-bar';
|
||||
import { ActivatedRoute, Router } from '@angular/router';
|
||||
import { Router } from '@angular/router';
|
||||
|
||||
import { CostCentre } from '../../core/cost-centre';
|
||||
import { ErrorStateComponent } from '../../shared/error-state/error-state.component';
|
||||
import { SkeletonLoaderComponent } from '../../shared/skeleton-loader/skeleton-loader.component';
|
||||
import { CostCentreService } from '../cost-centre.service';
|
||||
|
||||
export interface CostCentreDetailFormData {
|
||||
name: string;
|
||||
}
|
||||
|
||||
@Component({
|
||||
selector: 'app-cost-centre-detail',
|
||||
templateUrl: './cost-centre-detail.component.html',
|
||||
styleUrls: ['./cost-centre-detail.component.css'],
|
||||
imports: [ReactiveFormsModule, MatFormFieldModule, MatInputModule, MatButtonModule],
|
||||
imports: [
|
||||
FormField,
|
||||
FormRoot,
|
||||
MatFormFieldModule,
|
||||
MatInputModule,
|
||||
MatButtonModule,
|
||||
SkeletonLoaderComponent,
|
||||
ErrorStateComponent,
|
||||
],
|
||||
})
|
||||
export class CostCentreDetailComponent implements OnInit, AfterViewInit {
|
||||
private route = inject(ActivatedRoute);
|
||||
export class CostCentreDetailComponent {
|
||||
id = input(null, { transform: (v: string | null | undefined) => v ?? null });
|
||||
|
||||
private router = inject(Router);
|
||||
private snackBar = inject(MatSnackBar);
|
||||
private ser = inject(CostCentreService);
|
||||
|
||||
@ViewChild('nameElement', { static: true }) nameElement!: ElementRef<HTMLInputElement>;
|
||||
form: FormGroup<{
|
||||
name: FormControl<string | null>;
|
||||
}>;
|
||||
itemResource = this.ser.get(this.id);
|
||||
|
||||
item: CostCentre = new CostCentre();
|
||||
item = computed(() => this.itemResource.value() ?? new CostCentre());
|
||||
|
||||
model = linkedSignal({
|
||||
source: this.item,
|
||||
computation: (itemVal): CostCentreDetailFormData => ({
|
||||
name: itemVal.name ?? '',
|
||||
}),
|
||||
});
|
||||
|
||||
form = createForm(this.model);
|
||||
|
||||
constructor() {
|
||||
this.form = new FormGroup({
|
||||
name: new FormControl<string | null>(null),
|
||||
afterNextRender(() => {
|
||||
this.form().focusBoundControl();
|
||||
});
|
||||
}
|
||||
|
||||
ngOnInit() {
|
||||
this.route.data.subscribe((value) => {
|
||||
const data = value as { item: CostCentre };
|
||||
|
||||
this.showItem(data.item);
|
||||
});
|
||||
}
|
||||
|
||||
showItem(item: CostCentre) {
|
||||
this.item = item;
|
||||
this.form.setValue({
|
||||
name: this.item.name,
|
||||
});
|
||||
}
|
||||
|
||||
ngAfterViewInit() {
|
||||
setTimeout(() => {
|
||||
this.nameElement.nativeElement.focus();
|
||||
}, 0);
|
||||
}
|
||||
|
||||
save() {
|
||||
this.ser.saveOrUpdate(this.getItem()).subscribe({
|
||||
next: () => {
|
||||
@@ -62,14 +62,15 @@ export class CostCentreDetailComponent implements OnInit, AfterViewInit {
|
||||
this.router.navigateByUrl('/cost-centres');
|
||||
},
|
||||
error: (error) => {
|
||||
this.snackBar.open(error, 'Danger');
|
||||
this.snackBar.open(error as string, 'Danger');
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
getItem(): CostCentre {
|
||||
const formModel = this.form.value;
|
||||
this.item.name = formModel.name ?? '';
|
||||
return this.item;
|
||||
const formModel = this.model();
|
||||
const item = this.item();
|
||||
item.name = formModel.name ?? '';
|
||||
return item;
|
||||
}
|
||||
}
|
||||
@@ -1,18 +0,0 @@
|
||||
import { TestBed } from '@angular/core/testing';
|
||||
import { ResolveFn } from '@angular/router';
|
||||
|
||||
import { CostCentre } from '../core/cost-centre';
|
||||
import { costCentreListResolver } from './cost-centre-list.resolver';
|
||||
|
||||
describe('costCentreListResolver', () => {
|
||||
const executeResolver: ResolveFn<CostCentre[]> = (...resolverParameters) =>
|
||||
TestBed.runInInjectionContext(() => costCentreListResolver(...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 { CostCentre } from '../core/cost-centre';
|
||||
import { CostCentreService } from './cost-centre.service';
|
||||
|
||||
export const costCentreListResolver: ResolveFn<CostCentre[]> = () => {
|
||||
return inject(CostCentreService).list();
|
||||
};
|
||||
@@ -1,69 +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 } from 'rxjs/operators';
|
||||
|
||||
import { CostCentre } from '../../core/cost-centre';
|
||||
|
||||
/** Simple sort comparator for example ID/Name columns (for client-side sorting). */
|
||||
const compare = (a: string | number, b: string | number, isAsc: boolean) => (a < b ? -1 : 1) * (isAsc ? 1 : -1);
|
||||
export class CostCentreListDataSource extends DataSource<CostCentre> {
|
||||
constructor(
|
||||
public data: CostCentre[],
|
||||
private paginator?: MatPaginator,
|
||||
private sort?: MatSort,
|
||||
) {
|
||||
super();
|
||||
}
|
||||
|
||||
connect(): Observable<CostCentre[]> {
|
||||
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);
|
||||
}
|
||||
|
||||
// Set the paginators length
|
||||
if (this.paginator) {
|
||||
this.paginator.length = this.data.length;
|
||||
}
|
||||
|
||||
return merge(observableOf(this.data), ...dataMutations).pipe(
|
||||
map(() => this.getPagedData(this.getSortedData([...this.data]))),
|
||||
);
|
||||
}
|
||||
|
||||
disconnect() {}
|
||||
|
||||
private getPagedData(data: CostCentre[]) {
|
||||
if (this.paginator === undefined) {
|
||||
return data;
|
||||
}
|
||||
const startIndex = this.paginator.pageIndex * this.paginator.pageSize;
|
||||
return data.splice(startIndex, this.paginator.pageSize);
|
||||
}
|
||||
|
||||
private getSortedData(data: CostCentre[]) {
|
||||
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);
|
||||
default:
|
||||
return 0;
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -6,30 +6,36 @@
|
||||
</a>
|
||||
</h2>
|
||||
|
||||
<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]="['/cost-centres', row.id]">{{ 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 list.'" (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]="['/cost-centres', row.id]">{{ row.name }}</a></mat-cell
|
||||
>
|
||||
</ng-container>
|
||||
|
||||
<!-- Name Column -->
|
||||
<ng-container matColumnDef="isFixture">
|
||||
<mat-header-cell *matHeaderCellDef mat-sort-header>Is Fixture?</mat-header-cell>
|
||||
<mat-cell *matCellDef="let row">{{ row.isFixture }}</mat-cell>
|
||||
</ng-container>
|
||||
<!-- Name Column -->
|
||||
<ng-container matColumnDef="isFixture">
|
||||
<mat-header-cell *matHeaderCellDef mat-sort-header>Is Fixture?</mat-header-cell>
|
||||
<mat-cell *matCellDef="let row">{{ row.isFixture }}</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]="list().length"
|
||||
[pageIndex]="pageIndex()"
|
||||
[pageSize]="pageSize()"
|
||||
[pageSizeOptions]="[25, 50, 100, 250]"
|
||||
>
|
||||
</mat-paginator>
|
||||
}
|
||||
@@ -1,36 +1,77 @@
|
||||
import { Component, inject, OnInit, ViewChild } from '@angular/core';
|
||||
import { Component, computed, inject, signal } from '@angular/core';
|
||||
import { MatButtonModule } from '@angular/material/button';
|
||||
import { MatIconModule } from '@angular/material/icon';
|
||||
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 { RouterModule } from '@angular/router';
|
||||
|
||||
import { CostCentre } from '../../core/cost-centre';
|
||||
import { CostCentreListDataSource } from './cost-centre-list-datasource';
|
||||
import { ErrorStateComponent } from '../../shared/error-state/error-state.component';
|
||||
import { SkeletonLoaderComponent } from '../../shared/skeleton-loader/skeleton-loader.component';
|
||||
import { CostCentreService } from '../cost-centre.service';
|
||||
|
||||
@Component({
|
||||
selector: 'app-cost-centre-list',
|
||||
templateUrl: './cost-centre-list.component.html',
|
||||
styleUrls: ['./cost-centre-list.component.css'],
|
||||
imports: [RouterModule, MatIconModule, MatTableModule, MatSortModule, MatPaginatorModule, MatButtonModule],
|
||||
imports: [
|
||||
RouterModule,
|
||||
MatIconModule,
|
||||
MatTableModule,
|
||||
MatSortModule,
|
||||
MatPaginatorModule,
|
||||
MatButtonModule,
|
||||
SkeletonLoaderComponent,
|
||||
ErrorStateComponent,
|
||||
],
|
||||
})
|
||||
export class CostCentreListComponent implements OnInit {
|
||||
private route = inject(ActivatedRoute);
|
||||
export class CostCentreListComponent {
|
||||
private ser = inject(CostCentreService);
|
||||
|
||||
pageSize = signal(50);
|
||||
pageIndex = signal(0);
|
||||
sortActive = signal('');
|
||||
sortDirection = signal('');
|
||||
listResource = this.ser.list();
|
||||
list = computed(() => this.listResource.value() ?? []);
|
||||
sortedList = computed(() => {
|
||||
const data = this.list();
|
||||
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);
|
||||
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);
|
||||
});
|
||||
|
||||
@ViewChild(MatPaginator, { static: true }) paginator!: MatPaginator;
|
||||
@ViewChild(MatSort, { static: true }) sort!: MatSort;
|
||||
list: CostCentre[] = [];
|
||||
dataSource: CostCentreListDataSource = new CostCentreListDataSource(this.list, this.paginator, this.sort);
|
||||
/** Columns displayed in the table. Columns IDs can be added, removed, or reordered. */
|
||||
displayedColumns = ['name', 'isFixture'];
|
||||
handlePageEvent(e: PageEvent) {
|
||||
this.pageSize.set(e.pageSize);
|
||||
this.pageIndex.set(e.pageIndex);
|
||||
}
|
||||
|
||||
ngOnInit() {
|
||||
this.route.data.subscribe((value) => {
|
||||
const data = value as { list: CostCentre[] };
|
||||
|
||||
this.list = data.list;
|
||||
});
|
||||
this.dataSource = new CostCentreListDataSource(this.list, this.paginator, this.sort);
|
||||
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 { CostCentre } from '../core/cost-centre';
|
||||
import { costCentreResolver } from './cost-centre.resolver';
|
||||
|
||||
describe('costCentreResolver', () => {
|
||||
const executeResolver: ResolveFn<CostCentre> = (...resolverParameters) =>
|
||||
TestBed.runInInjectionContext(() => costCentreResolver(...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 { CostCentre } from '../core/cost-centre';
|
||||
import { CostCentreService } from './cost-centre.service';
|
||||
|
||||
export const costCentreResolver: ResolveFn<CostCentre> = (route) => {
|
||||
const id = route.paramMap.get('id');
|
||||
return inject(CostCentreService).get(id);
|
||||
};
|
||||
@@ -2,9 +2,7 @@ import { Routes } from '@angular/router';
|
||||
|
||||
import { authGuard } from '../auth/auth-guard.service';
|
||||
import { CostCentreDetailComponent } from './cost-centre-detail/cost-centre-detail.component';
|
||||
import { costCentreListResolver } from './cost-centre-list.resolver';
|
||||
import { CostCentreListComponent } from './cost-centre-list/cost-centre-list.component';
|
||||
import { costCentreResolver } from './cost-centre.resolver';
|
||||
|
||||
export const routes: Routes = [
|
||||
{
|
||||
@@ -14,9 +12,6 @@ export const routes: Routes = [
|
||||
data: {
|
||||
permission: 'Cost Centres',
|
||||
},
|
||||
resolve: {
|
||||
list: costCentreListResolver,
|
||||
},
|
||||
},
|
||||
{
|
||||
path: 'new',
|
||||
@@ -25,9 +20,6 @@ export const routes: Routes = [
|
||||
data: {
|
||||
permission: 'Cost Centres',
|
||||
},
|
||||
resolve: {
|
||||
item: costCentreResolver,
|
||||
},
|
||||
},
|
||||
{
|
||||
path: ':id',
|
||||
@@ -36,8 +28,5 @@ export const routes: Routes = [
|
||||
data: {
|
||||
permission: 'Cost Centres',
|
||||
},
|
||||
resolve: {
|
||||
item: costCentreResolver,
|
||||
},
|
||||
},
|
||||
];
|
||||
@@ -1,5 +1,5 @@
|
||||
import { HttpClient } from '@angular/common/http';
|
||||
import { inject, Injectable } from '@angular/core';
|
||||
import { HttpClient, httpResource } from '@angular/common/http';
|
||||
import { inject, Injectable, Signal } from '@angular/core';
|
||||
import { Observable } from 'rxjs/internal/Observable';
|
||||
import { catchError } from 'rxjs/operators';
|
||||
|
||||
@@ -7,8 +7,6 @@ import { CostCentre } from '../core/cost-centre';
|
||||
import { ErrorLoggerService } from '../core/error-logger.service';
|
||||
|
||||
const url = '/api/cost-centres';
|
||||
const serviceName = 'CostCentreService';
|
||||
|
||||
@Injectable({
|
||||
providedIn: 'root',
|
||||
})
|
||||
@@ -16,29 +14,27 @@ export class CostCentreService {
|
||||
private http = inject(HttpClient);
|
||||
private log = inject(ErrorLoggerService);
|
||||
|
||||
get(id: string | null): Observable<CostCentre> {
|
||||
const getUrl: string = id === null ? `${url}` : `${url}/${id}`;
|
||||
return this.http
|
||||
.get<CostCentre>(getUrl)
|
||||
.pipe(catchError(this.log.handleError(serviceName, `get id=${id}`))) as Observable<CostCentre>;
|
||||
get(id: Signal<string | null>) {
|
||||
return httpResource<CostCentre>(() => {
|
||||
const getUrl: string = id() === null ? `${url}` : `${url}/${id()}`;
|
||||
return getUrl;
|
||||
});
|
||||
}
|
||||
|
||||
list(): Observable<CostCentre[]> {
|
||||
return this.http
|
||||
.get<CostCentre[]>(`${url}/list`)
|
||||
.pipe(catchError(this.log.handleError(serviceName, 'list'))) as Observable<CostCentre[]>;
|
||||
list() {
|
||||
return httpResource<CostCentre[]>(() => `${url}/list`);
|
||||
}
|
||||
|
||||
save(costCentre: CostCentre): Observable<CostCentre> {
|
||||
return this.http
|
||||
.post<CostCentre>(`${url}`, costCentre)
|
||||
.pipe(catchError(this.log.handleError(serviceName, 'save'))) as Observable<CostCentre>;
|
||||
.pipe(catchError(this.log.handleError('CostCentreService', 'save'))) as Observable<CostCentre>;
|
||||
}
|
||||
|
||||
update(costCentre: CostCentre): Observable<CostCentre> {
|
||||
return this.http
|
||||
.put<CostCentre>(`${url}/${costCentre.id}`, costCentre)
|
||||
.pipe(catchError(this.log.handleError(serviceName, 'update'))) as Observable<CostCentre>;
|
||||
.pipe(catchError(this.log.handleError('CostCentreService', 'update'))) as Observable<CostCentre>;
|
||||
}
|
||||
|
||||
saveOrUpdate(costCentre: CostCentre): Observable<CostCentre> {
|
||||
@@ -51,6 +47,6 @@ export class CostCentreService {
|
||||
delete(id: string): Observable<CostCentre> {
|
||||
return this.http
|
||||
.delete<CostCentre>(`${url}/${id}`)
|
||||
.pipe(catchError(this.log.handleError(serviceName, 'delete'))) as Observable<CostCentre>;
|
||||
.pipe(catchError(this.log.handleError('CostCentreService', 'delete'))) as Observable<CostCentre>;
|
||||
}
|
||||
}
|
||||
@@ -1,73 +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 } from 'rxjs/operators';
|
||||
|
||||
import { DaybookItem } from './daybook-item';
|
||||
|
||||
/** Simple sort comparator for example ID/Name columns (for client-side sorting). */
|
||||
const compare = (a: string | number, b: string | number, isAsc: boolean) => (a < b ? -1 : 1) * (isAsc ? 1 : -1);
|
||||
export class DaybookDataSource extends DataSource<DaybookItem> {
|
||||
constructor(
|
||||
public data: DaybookItem[],
|
||||
private paginator?: MatPaginator,
|
||||
private sort?: MatSort,
|
||||
) {
|
||||
super();
|
||||
}
|
||||
|
||||
connect(): Observable<DaybookItem[]> {
|
||||
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);
|
||||
}
|
||||
|
||||
// Set the paginators length
|
||||
if (this.paginator) {
|
||||
this.paginator.length = this.data.length;
|
||||
}
|
||||
|
||||
return merge(observableOf(this.data), ...dataMutations).pipe(
|
||||
map(() => this.getPagedData(this.getSortedData([...this.data]))),
|
||||
);
|
||||
}
|
||||
|
||||
disconnect() {}
|
||||
|
||||
private getPagedData(data: DaybookItem[]) {
|
||||
if (this.paginator === undefined) {
|
||||
return data;
|
||||
}
|
||||
const startIndex = this.paginator.pageIndex * this.paginator.pageSize;
|
||||
return data.splice(startIndex, this.paginator.pageSize);
|
||||
}
|
||||
|
||||
private getSortedData(data: DaybookItem[]) {
|
||||
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 'date':
|
||||
return compare(a.date, b.date, isAsc);
|
||||
case 'debitAmount':
|
||||
return compare(+a.debitAmount, +b.debitAmount, isAsc);
|
||||
case 'creditAmount':
|
||||
return compare(+a.creditAmount, +b.creditAmount, isAsc);
|
||||
default:
|
||||
return 0;
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -1,81 +1,87 @@
|
||||
<h2>Daybook</h2>
|
||||
|
||||
<form [formGroup]="form" class="flex-col">
|
||||
<form class="flex-col" [formRoot]="form">
|
||||
<div class="row-container">
|
||||
<mat-form-field class="flex-auto basis-2-5">
|
||||
<mat-label>Start Date</mat-label>
|
||||
<input matInput #startDateElement [matDatepicker]="startDate" formControlName="startDate" autocomplete="off" />
|
||||
<input matInput [matDatepicker]="startDate" [formField]="form.startDate" autocomplete="off" />
|
||||
<mat-datepicker-toggle matSuffix [for]="startDate"></mat-datepicker-toggle>
|
||||
<mat-datepicker #startDate></mat-datepicker>
|
||||
</mat-form-field>
|
||||
<mat-form-field class="flex-auto basis-2-5">
|
||||
<mat-label>Finish Date</mat-label>
|
||||
<input matInput [matDatepicker]="finishDate" formControlName="finishDate" autocomplete="off" />
|
||||
<input matInput [matDatepicker]="finishDate" [formField]="form.finishDate" autocomplete="off" />
|
||||
<mat-datepicker-toggle matSuffix [for]="finishDate"></mat-datepicker-toggle>
|
||||
<mat-datepicker #finishDate></mat-datepicker>
|
||||
</mat-form-field>
|
||||
<button mat-raised-button class="flex-auto basis-1-5" color="primary" (click)="show()">Show</button>
|
||||
</div>
|
||||
</form>
|
||||
<mat-table #table [dataSource]="dataSource" matSort aria-label="Elements">
|
||||
<!-- Date Column -->
|
||||
<ng-container matColumnDef="date">
|
||||
<mat-header-cell *matHeaderCellDef mat-sort-header>Date</mat-header-cell>
|
||||
<mat-cell *matCellDef="let row"
|
||||
><a [routerLink]="row.url">{{ row.date }}</a></mat-cell
|
||||
>
|
||||
</ng-container>
|
||||
@if (infoResource.isLoading()) {
|
||||
<app-skeleton-loader type="table" [count]="1" />
|
||||
} @else if (infoResource.error()) {
|
||||
<app-error-state [message]="'Failed to load data.'" (retryAction)="infoResource.reload()" />
|
||||
} @else {
|
||||
<mat-table #table [dataSource]="dataSource()" matSort (matSortChange)="sortData($event)">
|
||||
<!-- Date Column -->
|
||||
<ng-container matColumnDef="date">
|
||||
<mat-header-cell *matHeaderCellDef mat-sort-header>Date</mat-header-cell>
|
||||
<mat-cell *matCellDef="let row"
|
||||
><a [routerLink]="row.url">{{ row.date }}</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.type }}</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.type }}</mat-cell>
|
||||
</ng-container>
|
||||
|
||||
<!-- Narration Column -->
|
||||
<ng-container matColumnDef="narration">
|
||||
<mat-header-cell *matHeaderCellDef mat-sort-header>Narration</mat-header-cell>
|
||||
<mat-cell *matCellDef="let row">{{ row.narration }}</mat-cell>
|
||||
</ng-container>
|
||||
<!-- Narration Column -->
|
||||
<ng-container matColumnDef="narration">
|
||||
<mat-header-cell *matHeaderCellDef mat-sort-header>Narration</mat-header-cell>
|
||||
<mat-cell *matCellDef="let row">{{ row.narration }}</mat-cell>
|
||||
</ng-container>
|
||||
|
||||
<!-- DebitText Column -->
|
||||
<ng-container matColumnDef="debitText">
|
||||
<mat-header-cell *matHeaderCellDef mat-sort-header>Debit Accounts</mat-header-cell>
|
||||
<mat-cell *matCellDef="let row">{{ row.debitText }}</mat-cell>
|
||||
</ng-container>
|
||||
<!-- DebitText Column -->
|
||||
<ng-container matColumnDef="debitText">
|
||||
<mat-header-cell *matHeaderCellDef mat-sort-header>Debit Accounts</mat-header-cell>
|
||||
<mat-cell *matCellDef="let row">{{ row.debitText }}</mat-cell>
|
||||
</ng-container>
|
||||
|
||||
<!-- Debit Column -->
|
||||
<ng-container matColumnDef="debitAmount">
|
||||
<mat-header-cell *matHeaderCellDef mat-sort-header class="right">Amount</mat-header-cell>
|
||||
<mat-cell *matCellDef="let row" class="right">{{ row.debitAmount | currency: 'INR' }}</mat-cell>
|
||||
</ng-container>
|
||||
<!-- Debit Column -->
|
||||
<ng-container matColumnDef="debitAmount">
|
||||
<mat-header-cell *matHeaderCellDef mat-sort-header class="right">Amount</mat-header-cell>
|
||||
<mat-cell *matCellDef="let row" class="right">{{ row.debitAmount | currency: 'INR' }}</mat-cell>
|
||||
</ng-container>
|
||||
|
||||
<!-- CreditText Column -->
|
||||
<ng-container matColumnDef="creditText">
|
||||
<mat-header-cell *matHeaderCellDef mat-sort-header>Credit Accounts</mat-header-cell>
|
||||
<mat-cell *matCellDef="let row">{{ row.creditText }}</mat-cell>
|
||||
</ng-container>
|
||||
<!-- CreditText Column -->
|
||||
<ng-container matColumnDef="creditText">
|
||||
<mat-header-cell *matHeaderCellDef mat-sort-header>Credit Accounts</mat-header-cell>
|
||||
<mat-cell *matCellDef="let row">{{ row.creditText }}</mat-cell>
|
||||
</ng-container>
|
||||
|
||||
<!-- Credit Column -->
|
||||
<ng-container matColumnDef="creditAmount">
|
||||
<mat-header-cell *matHeaderCellDef mat-sort-header class="right">Amount</mat-header-cell>
|
||||
<mat-cell *matCellDef="let row" class="right">{{ row.creditAmount | currency: 'INR' }}</mat-cell>
|
||||
</ng-container>
|
||||
<!-- Credit Column -->
|
||||
<ng-container matColumnDef="creditAmount">
|
||||
<mat-header-cell *matHeaderCellDef mat-sort-header class="right">Amount</mat-header-cell>
|
||||
<mat-cell *matCellDef="let row" class="right">{{ row.creditAmount | currency: 'INR' }}</mat-cell>
|
||||
</ng-container>
|
||||
|
||||
<mat-header-row *matHeaderRowDef="displayedColumns"></mat-header-row>
|
||||
<mat-row
|
||||
*matRowDef="let row; columns: displayedColumns"
|
||||
[class.selected]="selectedRowId === row.id"
|
||||
[class.unposted]="!row.posted && selectedRowId !== row.id"
|
||||
(click)="selectRow(row.id)"
|
||||
></mat-row>
|
||||
</mat-table>
|
||||
<mat-header-row *matHeaderRowDef="displayedColumns"></mat-header-row>
|
||||
<mat-row
|
||||
*matRowDef="let row; columns: displayedColumns"
|
||||
[class.selected]="selectedRowId === row.id"
|
||||
[class.unposted]="!row.posted && selectedRowId !== row.id"
|
||||
(click)="selectRow(row.id)"
|
||||
></mat-row>
|
||||
</mat-table>
|
||||
|
||||
<mat-paginator
|
||||
#paginator
|
||||
[length]="dataSource.data.length"
|
||||
[pageIndex]="0"
|
||||
[pageSize]="50"
|
||||
[pageSizeOptions]="[25, 50, 100, 250, 300]"
|
||||
>
|
||||
</mat-paginator>
|
||||
<mat-paginator
|
||||
(page)="handlePageEvent($event)"
|
||||
[length]="dataSource().length"
|
||||
[pageIndex]="pageIndex()"
|
||||
[pageSize]="pageSize()"
|
||||
[pageSizeOptions]="[25, 50, 100, 250, 300]"
|
||||
>
|
||||
</mat-paginator>
|
||||
}
|
||||
@@ -1,25 +1,32 @@
|
||||
import { CurrencyPipe } from '@angular/common';
|
||||
import { Component, ElementRef, HostListener, inject, OnInit, ViewChild } from '@angular/core';
|
||||
import { FormControl, FormGroup, ReactiveFormsModule } from '@angular/forms';
|
||||
import { Component, HostListener, inject, linkedSignal, input, computed, afterNextRender, signal } from '@angular/core';
|
||||
import { form as createForm, FormField, FormRoot } from '@angular/forms/signals';
|
||||
export interface DaybookFormData {
|
||||
startDate: Date;
|
||||
finishDate: Date;
|
||||
}
|
||||
import { MatButtonModule } from '@angular/material/button';
|
||||
import { MatDatepickerModule } from '@angular/material/datepicker';
|
||||
import { MatFormFieldModule } from '@angular/material/form-field';
|
||||
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, Router, RouterModule } from '@angular/router';
|
||||
import { Router, RouterModule } from '@angular/router';
|
||||
import moment from 'moment';
|
||||
|
||||
import { ErrorStateComponent } from '../shared/error-state/error-state.component';
|
||||
import { SkeletonLoaderComponent } from '../shared/skeleton-loader/skeleton-loader.component';
|
||||
import { Daybook } from './daybook';
|
||||
import { DaybookDataSource } from './daybook-datasource';
|
||||
import { DaybookService } from './daybook.service';
|
||||
|
||||
@Component({
|
||||
selector: 'app-daybook',
|
||||
templateUrl: './daybook.component.html',
|
||||
styleUrls: ['./daybook.component.css'],
|
||||
imports: [
|
||||
ReactiveFormsModule,
|
||||
FormField,
|
||||
FormRoot,
|
||||
MatFormFieldModule,
|
||||
MatInputModule,
|
||||
MatDatepickerModule,
|
||||
@@ -29,21 +36,72 @@ import { DaybookDataSource } from './daybook-datasource';
|
||||
RouterModule,
|
||||
MatPaginatorModule,
|
||||
CurrencyPipe,
|
||||
|
||||
SkeletonLoaderComponent,
|
||||
ErrorStateComponent,
|
||||
],
|
||||
})
|
||||
export class DaybookComponent implements OnInit {
|
||||
private route = inject(ActivatedRoute);
|
||||
export class DaybookComponent {
|
||||
private router = inject(Router);
|
||||
private ser = inject(DaybookService);
|
||||
startDate = input(null, { transform: (v: string | null | undefined) => v ?? null });
|
||||
finishDate = input(null, { transform: (v: string | null | undefined) => v ?? null });
|
||||
pageSize = signal(50);
|
||||
pageIndex = signal(0);
|
||||
|
||||
@ViewChild(MatPaginator, { static: true }) paginator!: MatPaginator;
|
||||
@ViewChild(MatSort, { static: true }) sort!: MatSort;
|
||||
@ViewChild('startDateElement', { static: true }) startDate!: ElementRef<HTMLInputElement>;
|
||||
info: Daybook = new Daybook();
|
||||
dataSource: DaybookDataSource = new DaybookDataSource(this.info.body, this.paginator, this.sort);
|
||||
form: FormGroup<{
|
||||
startDate: FormControl<moment.Moment>;
|
||||
finishDate: FormControl<moment.Moment>;
|
||||
}>;
|
||||
sortActive = signal('');
|
||||
sortDirection = signal('');
|
||||
infoResource = this.ser.list(this.startDate, this.finishDate);
|
||||
info = computed(() => this.infoResource.value() ?? new Daybook());
|
||||
|
||||
sortedList = computed(() => {
|
||||
const data = this.info().body ?? [];
|
||||
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 'date':
|
||||
return compare(a.date, b.date, isAsc);
|
||||
case 'type':
|
||||
return compare(a.type, b.type, isAsc);
|
||||
case 'narration':
|
||||
return compare(a.narration ?? '', b.narration ?? '', isAsc);
|
||||
case 'debitText':
|
||||
return compare(a.debitText ?? '', b.debitText ?? '', isAsc);
|
||||
case 'debitAmount':
|
||||
return compare(a.debitAmount ?? 0, b.debitAmount ?? 0, isAsc);
|
||||
case 'creditText':
|
||||
return compare(a.creditText ?? '', b.creditText ?? '', isAsc);
|
||||
case 'creditAmount':
|
||||
return compare(a.creditAmount ?? 0, b.creditAmount ?? 0, 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);
|
||||
});
|
||||
|
||||
model = linkedSignal({
|
||||
source: this.info,
|
||||
computation: (info: Daybook): DaybookFormData => ({
|
||||
startDate: info.startDate ? moment(info.startDate, 'DD-MMM-YYYY').toDate() : new Date(),
|
||||
finishDate: info.finishDate ? moment(info.finishDate, 'DD-MMM-YYYY').toDate() : new Date(),
|
||||
}),
|
||||
});
|
||||
|
||||
form = createForm(this.model);
|
||||
|
||||
selectedRowId = '';
|
||||
/** Columns displayed in the table. Columns IDs can be added, removed, or reordered. */
|
||||
@@ -52,27 +110,13 @@ export class DaybookComponent implements OnInit {
|
||||
@HostListener('window:keydown.f2', ['$event'])
|
||||
focusDate(event: Event) {
|
||||
event.preventDefault();
|
||||
this.startDate.nativeElement.focus();
|
||||
this.startDate.nativeElement.select();
|
||||
// TODO: Also select the text
|
||||
this.form.startDate().focusBoundControl();
|
||||
}
|
||||
|
||||
constructor() {
|
||||
this.form = new FormGroup({
|
||||
startDate: new FormControl(moment(new Date()), { nonNullable: true }),
|
||||
finishDate: new FormControl(moment(new Date()), { nonNullable: true }),
|
||||
});
|
||||
}
|
||||
|
||||
ngOnInit() {
|
||||
this.route.data.subscribe((value) => {
|
||||
const data = value as { info: Daybook };
|
||||
|
||||
this.info = data.info;
|
||||
this.form.setValue({
|
||||
startDate: moment(this.info.startDate, 'DD-MMM-YYYY'),
|
||||
finishDate: moment(this.info.finishDate, 'DD-MMM-YYYY'),
|
||||
});
|
||||
this.dataSource = new DaybookDataSource(this.info.body, this.paginator, this.sort);
|
||||
afterNextRender(() => {
|
||||
this.form.startDate().focusBoundControl();
|
||||
});
|
||||
}
|
||||
|
||||
@@ -91,7 +135,7 @@ export class DaybookComponent implements OnInit {
|
||||
}
|
||||
|
||||
prepareSubmit(): Daybook {
|
||||
const formModel = this.form.value;
|
||||
const formModel = this.model();
|
||||
|
||||
return {
|
||||
startDate: moment(formModel.startDate).format('DD-MMM-YYYY'),
|
||||
@@ -99,4 +143,16 @@ export class DaybookComponent implements OnInit {
|
||||
body: [],
|
||||
};
|
||||
}
|
||||
|
||||
handlePageEvent(e: PageEvent) {
|
||||
this.pageSize.set(e.pageSize);
|
||||
this.pageIndex.set(e.pageIndex);
|
||||
}
|
||||
|
||||
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 { Daybook } from './daybook';
|
||||
import { daybookResolver } from './daybook.resolver';
|
||||
|
||||
describe('daybookResolver', () => {
|
||||
const executeResolver: ResolveFn<Daybook> = (...resolverParameters) =>
|
||||
TestBed.runInInjectionContext(() => daybookResolver(...resolverParameters));
|
||||
|
||||
beforeEach(() => {
|
||||
TestBed.configureTestingModule({});
|
||||
});
|
||||
|
||||
it('should be created', () => {
|
||||
expect(executeResolver).toBeTruthy();
|
||||
});
|
||||
});
|
||||
@@ -1,11 +0,0 @@
|
||||
import { inject } from '@angular/core';
|
||||
import { ResolveFn } from '@angular/router';
|
||||
|
||||
import { Daybook } from './daybook';
|
||||
import { DaybookService } from './daybook.service';
|
||||
|
||||
export const daybookResolver: ResolveFn<Daybook> = (route) => {
|
||||
const startDate = route.queryParamMap.get('startDate') || null;
|
||||
const finishDate = route.queryParamMap.get('finishDate') || null;
|
||||
return inject(DaybookService).list(startDate, finishDate);
|
||||
};
|
||||
@@ -2,7 +2,6 @@ import { Routes } from '@angular/router';
|
||||
|
||||
import { authGuard } from '../auth/auth-guard.service';
|
||||
import { DaybookComponent } from './daybook.component';
|
||||
import { daybookResolver } from './daybook.resolver';
|
||||
|
||||
export const routes: Routes = [
|
||||
{
|
||||
@@ -12,9 +11,6 @@ export const routes: Routes = [
|
||||
data: {
|
||||
permission: 'Daybook',
|
||||
},
|
||||
resolve: {
|
||||
info: daybookResolver,
|
||||
},
|
||||
runGuardsAndResolvers: 'always',
|
||||
},
|
||||
];
|
||||
@@ -1,31 +1,21 @@
|
||||
import { HttpClient, HttpParams } from '@angular/common/http';
|
||||
import { inject, Injectable } from '@angular/core';
|
||||
import { Observable } from 'rxjs/internal/Observable';
|
||||
import { catchError } from 'rxjs/operators';
|
||||
import { httpResource } from '@angular/common/http';
|
||||
import { Injectable, Signal } from '@angular/core';
|
||||
|
||||
import { ErrorLoggerService } from '../core/error-logger.service';
|
||||
import { Daybook } from './daybook';
|
||||
|
||||
const url = '/api/daybook';
|
||||
const serviceName = 'DaybookService';
|
||||
|
||||
@Injectable({
|
||||
providedIn: 'root',
|
||||
})
|
||||
export class DaybookService {
|
||||
private http = inject(HttpClient);
|
||||
private log = inject(ErrorLoggerService);
|
||||
|
||||
list(startDate: string | null, finishDate: string | null): Observable<Daybook> {
|
||||
const options = { params: new HttpParams() };
|
||||
if (startDate !== null) {
|
||||
options.params = options.params.set('s', startDate);
|
||||
}
|
||||
if (finishDate !== null) {
|
||||
options.params = options.params.set('f', finishDate);
|
||||
}
|
||||
return this.http
|
||||
.get<Daybook>(url, options)
|
||||
.pipe(catchError(this.log.handleError(serviceName, 'list'))) as Observable<Daybook>;
|
||||
list(startDate: Signal<string | null>, finishDate: Signal<string | null>) {
|
||||
return httpResource<Daybook>(() => {
|
||||
const params: Record<string, string> = {};
|
||||
const startDate_val = startDate();
|
||||
if (startDate_val !== null) params['s'] = startDate_val;
|
||||
const finishDate_val = finishDate();
|
||||
if (finishDate_val !== null) params['f'] = finishDate_val;
|
||||
return { url, params };
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -1,16 +0,0 @@
|
||||
import { DataSource } from '@angular/cdk/collections';
|
||||
import { Observable } from 'rxjs';
|
||||
|
||||
import { EmployeeAttendanceItem } from './employee-attendance-item';
|
||||
|
||||
export class EmployeeAttendanceDataSource extends DataSource<EmployeeAttendanceItem> {
|
||||
constructor(private data: Observable<EmployeeAttendanceItem[]>) {
|
||||
super();
|
||||
}
|
||||
|
||||
connect(): Observable<EmployeeAttendanceItem[]> {
|
||||
return this.data;
|
||||
}
|
||||
|
||||
disconnect() {}
|
||||
}
|
||||
@@ -1,93 +1,112 @@
|
||||
<h2>Employee Attendance</h2>
|
||||
|
||||
<form [formGroup]="form" class="flex-col">
|
||||
<form class="flex-col" [formRoot]="form">
|
||||
<div class="row-container">
|
||||
<mat-form-field class="flex-auto">
|
||||
<mat-form-field class="flex-auto basis-1-4">
|
||||
<mat-label>Start Date</mat-label>
|
||||
<input matInput #startDateElement [matDatepicker]="startDate" formControlName="startDate" autocomplete="off" />
|
||||
<input
|
||||
matInput
|
||||
#startDateElement
|
||||
[matDatepicker]="startDate"
|
||||
(focus)="startDateElement.select()"
|
||||
[formField]="form.startDate"
|
||||
autocomplete="off"
|
||||
/>
|
||||
<mat-datepicker-toggle matSuffix [for]="startDate"></mat-datepicker-toggle>
|
||||
<mat-datepicker #startDate></mat-datepicker>
|
||||
</mat-form-field>
|
||||
<mat-form-field class="flex-auto">
|
||||
<mat-form-field class="flex-auto basis-1-4">
|
||||
<mat-label>Finish Date</mat-label>
|
||||
<input matInput [matDatepicker]="finishDate" formControlName="finishDate" autocomplete="off" />
|
||||
<input
|
||||
matInput
|
||||
#finishDateElement
|
||||
[matDatepicker]="finishDate"
|
||||
(focus)="finishDateElement.select()"
|
||||
[formField]="form.finishDate"
|
||||
autocomplete="off"
|
||||
/>
|
||||
<mat-datepicker-toggle matSuffix [for]="finishDate"></mat-datepicker-toggle>
|
||||
<mat-datepicker #finishDate></mat-datepicker>
|
||||
</mat-form-field>
|
||||
</div>
|
||||
<div class="row-container">
|
||||
<mat-form-field class="flex-auto basis-4-5">
|
||||
<mat-form-field class="flex-auto basis-1-4">
|
||||
<mat-label>Employee</mat-label>
|
||||
<input
|
||||
type="text"
|
||||
placeholder="Select Employee"
|
||||
aria-label="Number"
|
||||
matInput
|
||||
[formField]="form.employee"
|
||||
[matAutocomplete]="autoEmployee"
|
||||
#employeeElement
|
||||
[matAutocomplete]="auto"
|
||||
formControlName="employee"
|
||||
autocomplete="off"
|
||||
/>
|
||||
<mat-autocomplete
|
||||
#auto="matAutocomplete"
|
||||
autoActiveFirstOption
|
||||
[displayWith]="displayFn"
|
||||
(optionSelected)="selected($event)"
|
||||
>
|
||||
@for (employee of employees | async; track employee) {
|
||||
<mat-option [value]="employee">{{ employee.name }}</mat-option>
|
||||
<mat-autocomplete autoActiveFirstOption #autoEmployee="matAutocomplete" [displayWith]="displayFn">
|
||||
@for (option of employees(); track option) {
|
||||
<mat-option [value]="option">
|
||||
{{ option.name }}
|
||||
</mat-option>
|
||||
}
|
||||
</mat-autocomplete>
|
||||
</mat-form-field>
|
||||
<button mat-raised-button class="flex-auto basis-1-5" color="primary" (click)="show()">Show</button>
|
||||
|
||||
<button mat-raised-button color="primary" (click)="show()" class="flex-auto basis-1-5">Show</button>
|
||||
</div>
|
||||
<mat-table #table [dataSource]="dataSource" formArrayName="attendances">
|
||||
<!-- Date Column -->
|
||||
<ng-container matColumnDef="date">
|
||||
<mat-header-cell *matHeaderCellDef class="center">Date</mat-header-cell>
|
||||
<mat-cell *matCellDef="let row" class="center">{{ row.date }}</mat-cell>
|
||||
</ng-container>
|
||||
@if (infoResource.isLoading()) {
|
||||
<app-skeleton-loader type="table" [count]="1" />
|
||||
} @else if (infoResource.error()) {
|
||||
<app-error-state [message]="'Failed to load data.'" (retryAction)="infoResource.reload()" />
|
||||
} @else {
|
||||
<mat-table #table [dataSource]="dataSource()">
|
||||
<!-- Date Column -->
|
||||
<ng-container matColumnDef="date">
|
||||
<mat-header-cell *matHeaderCellDef class="right basis-1-5">Date</mat-header-cell>
|
||||
<mat-cell *matCellDef="let row" class="right basis-1-5">{{ row.date }}</mat-cell>
|
||||
</ng-container>
|
||||
|
||||
<!-- Status Column -->
|
||||
<ng-container matColumnDef="status">
|
||||
<mat-header-cell *matHeaderCellDef>Status</mat-header-cell>
|
||||
<mat-cell *matCellDef="let row; let i = index" [formGroupName]="i" class="flex-auto">
|
||||
<mat-select formControlName="attendanceType" name="attendanceType">
|
||||
@for (at of attendanceTypes; track at) {
|
||||
<mat-option [value]="at.id">
|
||||
{{ at.name }}
|
||||
</mat-option>
|
||||
<!-- Status Column -->
|
||||
<ng-container matColumnDef="status">
|
||||
<mat-header-cell *matHeaderCellDef>Status</mat-header-cell>
|
||||
<mat-cell *matCellDef="let row; let i = index" class="flex-auto basis-1-2">
|
||||
@if (form.attendances[i]) {
|
||||
<mat-select [formField]="form.attendances[i].attendanceType">
|
||||
@for (at of attendanceTypes(); track at) {
|
||||
<mat-option [value]="at.id">
|
||||
{{ at.name }}
|
||||
</mat-option>
|
||||
}
|
||||
</mat-select>
|
||||
}
|
||||
</mat-select>
|
||||
</mat-cell>
|
||||
</ng-container>
|
||||
</mat-cell>
|
||||
</ng-container>
|
||||
|
||||
<!-- Prints Column -->
|
||||
<ng-container matColumnDef="prints">
|
||||
<mat-header-cell *matHeaderCellDef>Prints</mat-header-cell>
|
||||
<mat-cell *matCellDef="let row; let i = index" class="no-bg">
|
||||
{{ row.prints }}
|
||||
@if (!form.controls.attendances.controls[i].controls.attendanceType.pristine) {
|
||||
<mat-icon class="no-bg">new_releases</mat-icon>
|
||||
}
|
||||
<mat-chip-set class="no-bg">
|
||||
@if (row.hoursWorked.length) {
|
||||
<mat-chip-option
|
||||
class="no-bg"
|
||||
selected
|
||||
selectable="false"
|
||||
[color]="row.fullDay === true ? 'primary' : 'warn'"
|
||||
>
|
||||
{{ row.hoursWorked }}
|
||||
</mat-chip-option>
|
||||
<!-- Prints Column -->
|
||||
<ng-container matColumnDef="prints">
|
||||
<mat-header-cell *matHeaderCellDef>Prints</mat-header-cell>
|
||||
<mat-cell *matCellDef="let row; let i = index" class="no-bg">
|
||||
{{ row.prints }}
|
||||
@if (form.attendances[i] && form.attendances[i].attendanceType !== null) {
|
||||
<mat-icon class="no-bg">new_releases</mat-icon>
|
||||
}
|
||||
</mat-chip-set>
|
||||
</mat-cell>
|
||||
</ng-container>
|
||||
<mat-chip-set class="no-bg">
|
||||
@if (row.hoursWorked.length) {
|
||||
<mat-chip-option
|
||||
class="no-bg"
|
||||
selected
|
||||
selectable="false"
|
||||
[color]="row.fullDay === true ? 'primary' : 'warn'"
|
||||
>
|
||||
{{ row.hoursWorked }}
|
||||
</mat-chip-option>
|
||||
}
|
||||
</mat-chip-set>
|
||||
</mat-cell>
|
||||
</ng-container>
|
||||
|
||||
<mat-header-row *matHeaderRowDef="displayedColumns"></mat-header-row>
|
||||
<mat-row *matRowDef="let row; let i = index; columns: displayedColumns" [class]="getClass(i)"></mat-row>
|
||||
</mat-table>
|
||||
<mat-header-row *matHeaderRowDef="displayedColumns"></mat-header-row>
|
||||
<mat-row *matRowDef="let row; let i = index; columns: displayedColumns" [class]="getClass(i)"></mat-row>
|
||||
</mat-table>
|
||||
}
|
||||
</form>
|
||||
|
||||
<div class="row-container">
|
||||
<button mat-raised-button color="primary" (click)="save()" [disabled]="form.pristine">Save</button>
|
||||
<button mat-raised-button color="primary" (click)="save()">Save</button>
|
||||
</div>
|
||||
@@ -1,38 +1,50 @@
|
||||
import { AsyncPipe } from '@angular/common';
|
||||
import { AfterViewInit, Component, ElementRef, HostListener, inject, OnInit, ViewChild } from '@angular/core';
|
||||
import { FormArray, FormControl, FormGroup, ReactiveFormsModule } from '@angular/forms';
|
||||
import { MatAutocompleteModule, MatAutocompleteSelectedEvent } from '@angular/material/autocomplete';
|
||||
import {
|
||||
computed,
|
||||
Component,
|
||||
HostListener,
|
||||
inject,
|
||||
debounced,
|
||||
input,
|
||||
linkedSignal,
|
||||
afterNextRender,
|
||||
} from '@angular/core';
|
||||
import { form as createForm, FormField, FormRoot } from '@angular/forms/signals';
|
||||
import { MatAutocompleteModule } from '@angular/material/autocomplete';
|
||||
import { MatButtonModule } from '@angular/material/button';
|
||||
import { MatChipsModule } from '@angular/material/chips';
|
||||
import { MatOptionModule } from '@angular/material/core';
|
||||
import { MatDatepickerModule } from '@angular/material/datepicker';
|
||||
import { MatDialog } from '@angular/material/dialog';
|
||||
import { MatFormFieldModule } from '@angular/material/form-field';
|
||||
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 { MatTableModule } from '@angular/material/table';
|
||||
import { ActivatedRoute, Router } from '@angular/router';
|
||||
import { Router } from '@angular/router';
|
||||
import moment from 'moment';
|
||||
import { BehaviorSubject, Observable, of as observableOf } from 'rxjs';
|
||||
import { debounceTime, distinctUntilChanged, switchMap } from 'rxjs/operators';
|
||||
|
||||
import { AttendanceType } from '../attendance/attendance-type';
|
||||
import { AuthService } from '../auth/auth.service';
|
||||
import { AttendanceTypeService } from '../attendance/attendance-type.service';
|
||||
import { Employee } from '../employee/employee';
|
||||
import { EmployeeService } from '../employee/employee.service';
|
||||
import { ErrorStateComponent } from '../shared/error-state/error-state.component';
|
||||
import { SkeletonLoaderComponent } from '../shared/skeleton-loader/skeleton-loader.component';
|
||||
import { EmployeeAttendance } from './employee-attendance';
|
||||
import { EmployeeAttendanceDataSource } from './employee-attendance-datasource';
|
||||
import { EmployeeAttendanceItem } from './employee-attendance-item';
|
||||
import { EmployeeAttendanceService } from './employee-attendance.service';
|
||||
|
||||
export interface EmployeeAttendanceFormData {
|
||||
startDate: Date;
|
||||
finishDate: Date;
|
||||
employee: string | Employee | null;
|
||||
attendances: { attendanceType: number | null }[];
|
||||
}
|
||||
|
||||
@Component({
|
||||
selector: 'app-employee-attendance',
|
||||
templateUrl: './employee-attendance.component.html',
|
||||
styleUrls: ['./employee-attendance.component.css'],
|
||||
imports: [
|
||||
ReactiveFormsModule,
|
||||
FormField,
|
||||
FormRoot,
|
||||
MatFormFieldModule,
|
||||
MatInputModule,
|
||||
MatDatepickerModule,
|
||||
@@ -43,115 +55,89 @@ import { EmployeeAttendanceService } from './employee-attendance.service';
|
||||
MatSelectModule,
|
||||
MatIconModule,
|
||||
MatChipsModule,
|
||||
AsyncPipe,
|
||||
|
||||
SkeletonLoaderComponent,
|
||||
ErrorStateComponent,
|
||||
],
|
||||
})
|
||||
export class EmployeeAttendanceComponent implements OnInit, AfterViewInit {
|
||||
private route = inject(ActivatedRoute);
|
||||
export class EmployeeAttendanceComponent {
|
||||
private router = inject(Router);
|
||||
private dialog = inject(MatDialog);
|
||||
private snackBar = inject(MatSnackBar);
|
||||
private auth = inject(AuthService);
|
||||
private ser = inject(EmployeeAttendanceService);
|
||||
private employeeSer = inject(EmployeeService);
|
||||
private attendanceTypeSer = inject(AttendanceTypeService);
|
||||
id = input(null, { transform: (v: string | null | undefined) => v ?? null });
|
||||
startDate = input(null, { transform: (v: string | null | undefined) => v ?? null });
|
||||
finishDate = input(null, { transform: (v: string | null | undefined) => v ?? null });
|
||||
infoResource = this.ser.get(this.id, this.startDate, this.finishDate);
|
||||
info = computed(() => this.infoResource.value() ?? new EmployeeAttendance());
|
||||
attendanceTypesResource = this.attendanceTypeSer.list();
|
||||
attendanceTypes = computed(() => this.attendanceTypesResource.value() ?? []);
|
||||
|
||||
@ViewChild('employeeElement', { static: true }) employeeElement!: ElementRef<HTMLInputElement>;
|
||||
@ViewChild('startDateElement', { static: true }) startDate!: ElementRef<HTMLInputElement>;
|
||||
public employeeAttendanceObservable = new BehaviorSubject<EmployeeAttendanceItem[]>([]);
|
||||
dataSource: EmployeeAttendanceDataSource = new EmployeeAttendanceDataSource(this.employeeAttendanceObservable);
|
||||
model = linkedSignal({
|
||||
source: this.info,
|
||||
computation: (info): EmployeeAttendanceFormData => ({
|
||||
startDate: info.startDate ? moment(info.startDate, 'DD-MMM-YYYY').toDate() : new Date(),
|
||||
finishDate: info.finishDate ? moment(info.finishDate, 'DD-MMM-YYYY').toDate() : new Date(),
|
||||
employee: info.employee ?? null,
|
||||
attendances: info.body.map((x) => ({
|
||||
attendanceType: x.attendanceType.id,
|
||||
})),
|
||||
}),
|
||||
});
|
||||
|
||||
form: FormGroup<{
|
||||
startDate: FormControl<moment.Moment>;
|
||||
finishDate: FormControl<moment.Moment>;
|
||||
employee: FormControl<string | null>;
|
||||
attendances: FormArray<
|
||||
FormGroup<{
|
||||
attendanceType: FormControl<number>;
|
||||
}>
|
||||
>;
|
||||
}>;
|
||||
form = createForm(this.model);
|
||||
|
||||
info: EmployeeAttendance = new EmployeeAttendance();
|
||||
attendanceTypes: AttendanceType[] = [];
|
||||
dataSource = computed(() => this.info().body);
|
||||
|
||||
displayedColumns = ['date', 'status', 'prints'];
|
||||
|
||||
employees: Observable<Employee[]>;
|
||||
employeesSearch = computed(() => {
|
||||
const x = this.model().employee;
|
||||
return typeof x === 'string' ? x : null;
|
||||
});
|
||||
|
||||
debouncedEmployeeSearch = debounced(this.employeesSearch, 150);
|
||||
employeesResource = this.employeeSer.autocomplete(computed(() => this.debouncedEmployeeSearch.value() ?? ''));
|
||||
employees = computed(() => this.employeesResource.value() ?? []);
|
||||
|
||||
@HostListener('window:keydown.f2', ['$event'])
|
||||
focusDate(event: Event) {
|
||||
event.preventDefault();
|
||||
this.startDate.nativeElement.focus();
|
||||
this.startDate.nativeElement.select();
|
||||
this.form().focusBoundControl();
|
||||
}
|
||||
|
||||
constructor() {
|
||||
this.form = new FormGroup({
|
||||
startDate: new FormControl(moment(new Date()), { nonNullable: true }),
|
||||
finishDate: new FormControl(moment(new Date()), { nonNullable: true }),
|
||||
employee: new FormControl<string | null>(null),
|
||||
attendances: new FormArray<FormGroup<{ attendanceType: FormControl<number> }>>([]),
|
||||
afterNextRender(() => {
|
||||
this.form().focusBoundControl();
|
||||
});
|
||||
// Listen to Employee Value Changes
|
||||
this.employees = this.form.controls.employee.valueChanges.pipe(
|
||||
debounceTime(150),
|
||||
distinctUntilChanged(),
|
||||
switchMap((x) => (x === null ? observableOf([]) : this.employeeSer.autocomplete(x))),
|
||||
);
|
||||
}
|
||||
|
||||
ngOnInit() {
|
||||
this.route.data.subscribe((value) => {
|
||||
const data = value as { info: EmployeeAttendance; attendanceTypes: AttendanceType[] };
|
||||
|
||||
this.info = data.info;
|
||||
this.attendanceTypes = data.attendanceTypes;
|
||||
this.form.controls.startDate.setValue(moment(this.info.startDate, 'DD-MMM-YYYY'));
|
||||
this.form.controls.finishDate.setValue(moment(this.info.finishDate, 'DD-MMM-YYYY'));
|
||||
this.form.controls.employee.setValue(this.info.employee?.name ?? '');
|
||||
this.form.controls.attendances.clear();
|
||||
this.info.body.forEach((x) =>
|
||||
this.form.controls.attendances.push(
|
||||
new FormGroup({
|
||||
attendanceType: new FormControl(x.attendanceType.id, { nonNullable: true }),
|
||||
}),
|
||||
),
|
||||
);
|
||||
this.dataSource = new EmployeeAttendanceDataSource(this.employeeAttendanceObservable);
|
||||
this.employeeAttendanceObservable.next(this.info.body);
|
||||
});
|
||||
}
|
||||
|
||||
ngAfterViewInit() {
|
||||
setTimeout(() => {
|
||||
this.employeeElement.nativeElement.focus();
|
||||
}, 0);
|
||||
}
|
||||
|
||||
displayFn(employee?: Employee | string): string {
|
||||
return !employee ? '' : typeof employee === 'string' ? employee : employee.name;
|
||||
}
|
||||
|
||||
selected(event: MatAutocompleteSelectedEvent): void {
|
||||
this.info.employee = event.option.value as Employee;
|
||||
}
|
||||
|
||||
getClass(index: number) {
|
||||
const array = this.form.controls.attendances;
|
||||
const id = array.controls[index].value.attendanceType;
|
||||
const { name } = this.attendanceTypes.filter((x) => x.id === id)[0];
|
||||
return name.toLowerCase().replace(/(\s+\+\s+)|(\s+)/g, '-');
|
||||
const array = this.model().attendances;
|
||||
if (!array || !array[index]) return '';
|
||||
const id = array[index].attendanceType;
|
||||
const typeObj = this.attendanceTypes().filter((x) => x.id === id)[0];
|
||||
if (!typeObj) return '';
|
||||
return typeObj.name.toLowerCase().replace(/(\s+\+\s+)|(\s+)/g, '-');
|
||||
}
|
||||
|
||||
show() {
|
||||
const formValue = this.form.value;
|
||||
this.info.startDate = moment(formValue.startDate).format('DD-MMM-YYYY');
|
||||
this.info.finishDate = moment(formValue.finishDate).format('DD-MMM-YYYY');
|
||||
if (this.info.employee) {
|
||||
this.router.navigate(['/employee-attendance', this.info.employee.id], {
|
||||
const formValue = this.model();
|
||||
const currentInfo = this.info();
|
||||
currentInfo.startDate = moment(formValue.startDate).format('DD-MMM-YYYY');
|
||||
currentInfo.finishDate = moment(formValue.finishDate).format('DD-MMM-YYYY');
|
||||
currentInfo.employee =
|
||||
typeof formValue.employee !== 'string' && formValue.employee ? formValue.employee : undefined;
|
||||
if (currentInfo.employee) {
|
||||
this.router.navigate(['/employee-attendance', currentInfo.employee.id], {
|
||||
queryParams: {
|
||||
startDate: this.info.startDate,
|
||||
finishDate: this.info.finishDate,
|
||||
startDate: currentInfo.startDate,
|
||||
finishDate: currentInfo.finishDate,
|
||||
},
|
||||
});
|
||||
}
|
||||
@@ -163,21 +149,26 @@ export class EmployeeAttendanceComponent implements OnInit, AfterViewInit {
|
||||
this.snackBar.open('', 'Success');
|
||||
},
|
||||
error: (error) => {
|
||||
this.snackBar.open(error, 'Danger');
|
||||
this.snackBar.open(error as string, 'Danger');
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
getEmployeeAttendance(): EmployeeAttendance {
|
||||
const formValue = this.form.value;
|
||||
this.info.startDate = moment(formValue.startDate).format('DD-MMM-YYYY');
|
||||
this.info.finishDate = moment(formValue.finishDate).format('DD-MMM-YYYY');
|
||||
const array = this.form.value.attendances;
|
||||
const formValue = this.model();
|
||||
const currentInfo = this.info();
|
||||
currentInfo.startDate = moment(formValue.startDate).format('DD-MMM-YYYY');
|
||||
currentInfo.finishDate = moment(formValue.finishDate).format('DD-MMM-YYYY');
|
||||
currentInfo.employee =
|
||||
typeof formValue.employee !== 'string' && formValue.employee ? formValue.employee : undefined;
|
||||
const array = formValue.attendances;
|
||||
if (array) {
|
||||
this.info.body.forEach((item, index) => {
|
||||
item.attendanceType.id = array[index].attendanceType ?? 0;
|
||||
currentInfo.body.forEach((item, index) => {
|
||||
if (array[index]) {
|
||||
item.attendanceType.id = array[index].attendanceType ?? 0;
|
||||
}
|
||||
});
|
||||
}
|
||||
return this.info;
|
||||
return currentInfo;
|
||||
}
|
||||
}
|
||||
@@ -1,12 +0,0 @@
|
||||
import { inject } from '@angular/core';
|
||||
import { ResolveFn } from '@angular/router';
|
||||
|
||||
import { EmployeeAttendance } from './employee-attendance';
|
||||
import { EmployeeAttendanceService } from './employee-attendance.service';
|
||||
|
||||
export const employeeAttendanceResolver: ResolveFn<EmployeeAttendance> = (route) => {
|
||||
const id = route.paramMap.get('id');
|
||||
const startDate = route.queryParamMap.get('startDate') || null;
|
||||
const finishDate = route.queryParamMap.get('finishDate') || null;
|
||||
return inject(EmployeeAttendanceService).get(id, startDate, finishDate);
|
||||
};
|
||||
@@ -1,9 +1,7 @@
|
||||
import { Routes } from '@angular/router';
|
||||
|
||||
import { attendanceTypeResolver } from '../attendance/attendance-type.resolver';
|
||||
import { authGuard } from '../auth/auth-guard.service';
|
||||
import { EmployeeAttendanceComponent } from './employee-attendance.component';
|
||||
import { employeeAttendanceResolver } from './employee-attendance.resolver';
|
||||
|
||||
export const routes: Routes = [
|
||||
{
|
||||
@@ -13,10 +11,6 @@ export const routes: Routes = [
|
||||
data: {
|
||||
permission: 'Attendance',
|
||||
},
|
||||
resolve: {
|
||||
info: employeeAttendanceResolver,
|
||||
attendanceTypes: attendanceTypeResolver,
|
||||
},
|
||||
runGuardsAndResolvers: 'always',
|
||||
},
|
||||
{
|
||||
@@ -26,10 +20,6 @@ export const routes: Routes = [
|
||||
data: {
|
||||
permission: 'Attendance',
|
||||
},
|
||||
resolve: {
|
||||
info: employeeAttendanceResolver,
|
||||
attendanceTypes: attendanceTypeResolver,
|
||||
},
|
||||
runGuardsAndResolvers: 'always',
|
||||
},
|
||||
];
|
||||
@@ -1,40 +1,36 @@
|
||||
import { HttpClient, HttpParams } from '@angular/common/http';
|
||||
import { inject, Injectable } from '@angular/core';
|
||||
import { Observable, of as observableOf } from 'rxjs';
|
||||
import { HttpClient, httpResource } from '@angular/common/http';
|
||||
import { inject, Injectable, Signal } from '@angular/core';
|
||||
import { Observable } from 'rxjs';
|
||||
import { catchError } from 'rxjs/operators';
|
||||
|
||||
import { ErrorLoggerService } from '../core/error-logger.service';
|
||||
import { EmployeeAttendance } from './employee-attendance';
|
||||
|
||||
const url = '/api/employee-attendance';
|
||||
const serviceName = 'EmployeeAttendanceService';
|
||||
|
||||
@Injectable({ providedIn: 'root' })
|
||||
export class EmployeeAttendanceService {
|
||||
private http = inject(HttpClient);
|
||||
private log = inject(ErrorLoggerService);
|
||||
|
||||
get(id: string | null, startDate: string | null, finishDate: string | null): Observable<EmployeeAttendance> {
|
||||
const getUrl: string = id === null ? url : `${url}/${id}`;
|
||||
const options = { params: new HttpParams() };
|
||||
if (startDate !== null) {
|
||||
options.params = options.params.set('s', startDate);
|
||||
}
|
||||
if (finishDate !== null) {
|
||||
options.params = options.params.set('f', finishDate);
|
||||
}
|
||||
return this.http
|
||||
.get<EmployeeAttendance>(getUrl, options)
|
||||
.pipe(catchError(this.log.handleError(serviceName, `get id=${id}`))) as Observable<EmployeeAttendance>;
|
||||
get(id: Signal<string | null>, startDate: Signal<string | null>, finishDate: Signal<string | null>) {
|
||||
return httpResource<EmployeeAttendance>(() => {
|
||||
const getUrl: string = id() === null ? `${url}` : `${url}/${id()}`;
|
||||
const params: Record<string, string> = {};
|
||||
const startDate_val = startDate();
|
||||
if (startDate_val !== null) params['s'] = startDate_val;
|
||||
const finishDate_val = finishDate();
|
||||
if (finishDate_val !== null) params['f'] = finishDate_val;
|
||||
return { url: getUrl, params };
|
||||
});
|
||||
}
|
||||
|
||||
save(employeeAttendance: EmployeeAttendance): Observable<EmployeeAttendance> {
|
||||
if (!employeeAttendance.employee) {
|
||||
return observableOf(new EmployeeAttendance());
|
||||
const id = employeeAttendance.employee?.id;
|
||||
if (!id) {
|
||||
throw new Error('Employee not chosen');
|
||||
}
|
||||
const { id } = employeeAttendance.employee;
|
||||
return this.http
|
||||
.post<EmployeeAttendance>(`${url}/${id}`, employeeAttendance)
|
||||
.pipe(catchError(this.log.handleError(serviceName, 'save'))) as Observable<EmployeeAttendance>;
|
||||
.pipe(catchError(this.log.handleError('EmployeeAttendanceService', 'save'))) as Observable<EmployeeAttendance>;
|
||||
}
|
||||
}
|
||||
@@ -1,16 +0,0 @@
|
||||
import { DataSource } from '@angular/cdk/collections';
|
||||
import { Observable } from 'rxjs';
|
||||
|
||||
import { EmployeeBenefit } from '../core/employee-benefit';
|
||||
|
||||
export class EmployeeBenefitsDataSource extends DataSource<EmployeeBenefit> {
|
||||
constructor(private data: Observable<EmployeeBenefit[]>) {
|
||||
super();
|
||||
}
|
||||
|
||||
connect(): Observable<EmployeeBenefit[]> {
|
||||
return this.data;
|
||||
}
|
||||
|
||||
disconnect() {}
|
||||
}
|
||||
@@ -1,6 +1,6 @@
|
||||
<h2>Employee Benefits</h2>
|
||||
|
||||
<form [formGroup]="form" class="flex-col wrapped">
|
||||
<form class="flex-col wrapped" [formRoot]="form">
|
||||
<div class="row-container">
|
||||
<mat-form-field class="flex-auto">
|
||||
<mat-label>Date</mat-label>
|
||||
@@ -9,14 +9,14 @@
|
||||
#dateElement
|
||||
[matDatepicker]="date"
|
||||
(focus)="dateElement.select()"
|
||||
formControlName="date"
|
||||
[formField]="form.date"
|
||||
autocomplete="off"
|
||||
/>
|
||||
<mat-datepicker-toggle matSuffix [for]="date"></mat-datepicker-toggle>
|
||||
<mat-datepicker #date></mat-datepicker>
|
||||
</mat-form-field>
|
||||
</div>
|
||||
<div formGroupName="addRow" class="row-container">
|
||||
<div class="row-container">
|
||||
<mat-form-field class="flex-auto basis-3-5">
|
||||
<mat-label>Employee</mat-label>
|
||||
<input
|
||||
@@ -24,32 +24,27 @@
|
||||
matInput
|
||||
#employeeElement
|
||||
[matAutocomplete]="auto"
|
||||
formControlName="employee"
|
||||
[formField]="form.addRow.employee"
|
||||
autocomplete="off"
|
||||
/>
|
||||
<mat-autocomplete
|
||||
#auto="matAutocomplete"
|
||||
autoActiveFirstOption
|
||||
[displayWith]="displayFn"
|
||||
(optionSelected)="employeeSelected($event)"
|
||||
>
|
||||
@for (employee of employees | async; track employee) {
|
||||
<mat-autocomplete #auto="matAutocomplete" autoActiveFirstOption [displayWith]="displayFn">
|
||||
@for (employee of employees(); track employee) {
|
||||
<mat-option [value]="employee">{{ employee.name }}</mat-option>
|
||||
}
|
||||
</mat-autocomplete>
|
||||
</mat-form-field>
|
||||
<mat-form-field class="flex-auto basis-15">
|
||||
<mat-label>Gross Salary</mat-label>
|
||||
<input type="text" matInput formControlName="grossSalary" autocomplete="off" />
|
||||
<input type="text" matInput [formField]="form.addRow.grossSalary" autocomplete="off" />
|
||||
</mat-form-field>
|
||||
<mat-form-field class="flex-auto basis-15">
|
||||
<mat-label>Days Worked</mat-label>
|
||||
<input type="text" matInput formControlName="daysWorked" autocomplete="off" />
|
||||
<input type="text" matInput [formField]="form.addRow.daysWorked" autocomplete="off" />
|
||||
</mat-form-field>
|
||||
|
||||
<button mat-raised-button color="primary" (click)="addRow()" class="flex-auto basis-10">Add</button>
|
||||
</div>
|
||||
<mat-table #table [dataSource]="dataSource">
|
||||
<mat-table #table [dataSource]="form.benefits().value() ?? []">
|
||||
<!-- Name Column -->
|
||||
<ng-container matColumnDef="name">
|
||||
<mat-header-cell *matHeaderCellDef>Name</mat-header-cell>
|
||||
@@ -121,19 +116,19 @@
|
||||
|
||||
<div class="row-container">
|
||||
<button mat-raised-button color="primary" (click)="save()" [disabled]="!canSave()">
|
||||
{{ voucher.id ? 'Update' : 'Save' }}
|
||||
{{ id() ? 'Update' : 'Save' }}
|
||||
</button>
|
||||
@if (voucher.id) {
|
||||
<button mat-raised-button (click)="post()" [disabled]="voucher.posted || !auth.allowed('post-vouchers')">
|
||||
{{ voucher.posted ? 'Posted' : 'Post' }}
|
||||
@if (id()) {
|
||||
<button mat-raised-button (click)="post()" [disabled]="item().posted || !auth.allowed('post-vouchers')">
|
||||
{{ item().posted ? 'Posted' : 'Post' }}
|
||||
</button>
|
||||
<button mat-raised-button color="warn" (click)="confirmDelete()" [disabled]="!canSave()">Delete</button>
|
||||
}
|
||||
</div>
|
||||
@if (voucher.id) {
|
||||
@if (id()) {
|
||||
<div class="row-container">
|
||||
Created on <strong>{{ voucher.creationDate | localTime }}</strong> and Last Edited on
|
||||
<strong>{{ voucher.lastEditDate | localTime }}</strong> by <strong>{{ voucher.user.name }}</strong
|
||||
>. {{ voucher.poster ? 'Posted by ' + voucher.poster : '' }}
|
||||
Created on <strong>{{ item().creationDate | localTime }}</strong> and Last Edited on
|
||||
<strong>{{ item().lastEditDate | localTime }}</strong> by <strong>{{ item().user.name }}</strong
|
||||
>. {{ item().poster ? 'Posted by ' + item().poster : '' }}
|
||||
</div>
|
||||
}
|
||||
Loaded 100 of 443 files, more files were not shown because too many files have changed in this diff.
Show more
Reference in new issue
Block a user